From 4275abb548719920c607cdebe69bfb3f93a09e53 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 6 Dec 2025 06:17:00 +0000 Subject: [PATCH 1/3] Initial plan From 53fbb9ae7057419ec4346bf7a3210bf3b4327026 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 6 Dec 2025 06:23:23 +0000 Subject: [PATCH 2/3] Add comprehensive documentation for QueryableExtensions, EnumExtensions, and other utilities Co-authored-by: mitchelsellers <5659113+mitchelsellers@users.noreply.github.com> --- README.md | 233 +++++++++++++++++++++- docs/EncryptionServices.md | 381 ++++++++++++++++++++++++++++++++++++ docs/EnumExtensions.md | 312 +++++++++++++++++++++++++++++ docs/QueryableExtensions.md | 239 ++++++++++++++++++++++ docs/README.md | 111 +++++++++++ 5 files changed, 1272 insertions(+), 4 deletions(-) create mode 100644 docs/EncryptionServices.md create mode 100644 docs/EnumExtensions.md create mode 100644 docs/QueryableExtensions.md create mode 100644 docs/README.md diff --git a/README.md b/README.md index 8e7742b..90de23c 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,14 @@ ICG.NetCore.Utilities ![](https://img.shields.io/nuget/v/icg.netcore.utilities.s [![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=IowaComputerGurus_netcore.utilities&metric=security_rating)](https://sonarcloud.io/dashboard?id=IowaComputerGurus_netcore.utilities) [![Technical Debt](https://sonarcloud.io/api/project_badges/measure?project=IowaComputerGurus_netcore.utilities&metric=sqale_index)](https://sonarcloud.io/dashboard?id=IowaComputerGurus_netcore.utilities) +## Documentation + +📚 **[View Detailed Documentation](docs/README.md)** - Comprehensive guides with examples and best practices + +Quick links: +- [QueryableExtensions](docs/QueryableExtensions.md) - Conditional LINQ operations +- [EnumExtensions](docs/EnumExtensions.md) - Display attribute helpers +- [Encryption Services](docs/EncryptionServices.md) - AES encryption services ## Usage @@ -33,18 +41,235 @@ Inside of of your project's Startus.cs within the RegisterServices method add th services.UseIcgNetCoreUtilities(); ``` -### Included C# Objects +### Included C# Objects and Services + +#### Provider Interfaces (for Testability) | Object | Purpose | | ---- | --- | -| IDirectory Provider | Provides a shim around the System.IO.Directory object to allow for unit testing. | +| IDirectoryProvider | Provides a shim around the System.IO.Directory object to allow for unit testing. | | IGuidProvider | Provides a shim around the System.Guid object to allow for unit testing of Guid operations. | | IFileProvider | Provides a shim around the System.IO.File object to allow for unit testing of file related operations. | | IPathProvider | Provides a shim around the System.IO.Path object to allow for unit testing of path related operations | | ITimeProvider | Provides a shim around the System.DateTime object to allow for unit testing of date operations | | ITimeSpanProvider | Provides a shim around the System.TimeSpan object to allow for unit testing/injection of TimeSpan operations | + +#### Services + +| Service | Purpose | +| ---- | --- | | IUrlSlugGenerator | Provides a service that will take input and generate a url friendly slug from the content | -| IAesEncryptionService | Provides a service that will encrypt and decrypt provided strings using AES symmetric encryption | +| IAesEncryptionService | Provides a service that will encrypt and decrypt provided strings using AES symmetric encryption with configured key and IV | +| IAesDerivedKeyEncryptionService | Provides a service that will encrypt and decrypt provided strings using AES encryption with derived keys from passphrase and salt | +| IDatabaseEnvironmentModelFactory | Factory to create DatabaseEnvironmentModel objects from connection strings for display purposes | + +#### Extension Methods + +| Extension Class | Purpose | +| ---- | --- | +| QueryableExtensions | Extension methods for IQueryable to simplify conditional filtering, ordering, paging, and distinct selection | +| EnumExtensions | Extension methods for Enum types to help display formatted enum values with Display attributes | +| IdentityExtensions | Extension methods for IIdentity objects to extract claim values | + +#### Constants + +| Class | Purpose | +| ---- | --- | +| Timezones | Helper constants for standard US timezone values | + +## Detailed Documentation + +### QueryableExtensions + +Extension methods for `IQueryable` that provide conditional querying capabilities. + +**Available Methods:** + +- **WhereIf** - Conditionally applies a filter to the query + ```csharp + var results = dbContext.Users + .WhereIf(filterByActive, u => u.IsActive) + .WhereIf(!string.IsNullOrEmpty(searchTerm), u => u.Name.Contains(searchTerm)); + ``` + +- **OrderByIf** - Conditionally applies ascending order to the query + ```csharp + var results = dbContext.Users + .OrderByIf(sortByName, u => u.Name); + ``` + +- **OrderByDescendingIf** - Conditionally applies descending order to the query + ```csharp + var results = dbContext.Users + .OrderByDescendingIf(sortByNewest, u => u.CreatedDate); + ``` + +- **GetPage** - Returns a specific page of results (1-based page numbers) + ```csharp + var results = dbContext.Users + .OrderBy(u => u.Name) + .GetPage(pageNumber: 2, pageSize: 10); + ``` + +- **DistinctBy** - Returns distinct elements by a specified key selector + ```csharp + var uniqueUsers = dbContext.Users + .DistinctBy(u => u.Email); + ``` + +### EnumExtensions + +Extension methods for working with Enum types and their Display attributes. + +**Available Methods:** + +- **GetDisplayNameOrStringValue** - Returns the Display Name attribute value or the enum's string value + ```csharp + public enum Status + { + [Display(Name = "Active User")] + Active, + Inactive + } + + var displayName = Status.Active.GetDisplayNameOrStringValue(); // "Active User" + var defaultName = Status.Inactive.GetDisplayNameOrStringValue(); // "Inactive" + ``` + +- **GetDisplayName** - Gets the Display Name attribute value (throws if not found) + ```csharp + var displayName = Status.Active.GetDisplayName(); // "Active User" + ``` + +- **HasDisplayName** - Checks if an enum value has a Display attribute + ```csharp + var hasDisplay = Status.Active.HasDisplayName(); // true + var hasDisplay = Status.Inactive.HasDisplayName(); // false + ``` + +### IdentityExtensions + +Extension methods for working with IIdentity and claims. + +**Available Methods:** + +- **GetClaimValue** - Extracts the value of a specific claim type from a ClaimsIdentity + ```csharp + // In a Razor view or controller + var firstName = User.Identity.GetClaimValue("Profile:FirstName"); + var email = User.Identity.GetClaimValue(ClaimTypes.Email); + ``` + +### AesEncryptionService + +Service for AES symmetric encryption with pre-configured key and IV values. + +**Configuration:** +```csharp +services.Configure(options => +{ + options.Key = "your-base64-encoded-key"; + options.IV = "your-base64-encoded-iv"; +}); +``` + +**Usage:** +```csharp +public class MyService +{ + private readonly IAesEncryptionService _encryptionService; + + public MyService(IAesEncryptionService encryptionService) + { + _encryptionService = encryptionService; + } + + public void EncryptData() + { + var encrypted = _encryptionService.Encrypt("sensitive data"); + var decrypted = _encryptionService.Decrypt(encrypted); + } +} +``` + +### AesDerivedKeyEncryptionService + +Service for AES encryption using derived keys from a passphrase and salt (using Rfc2898DeriveBytes). + +**Configuration:** +```csharp +services.Configure(options => +{ + options.Passphrase = "your-secure-passphrase"; +}); +``` + +**Usage:** +```csharp +public class MyService +{ + private readonly IAesDerivedKeyEncryptionService _encryptionService; + + public MyService(IAesDerivedKeyEncryptionService encryptionService) + { + _encryptionService = encryptionService; + } + + public void EncryptData() + { + var salt = "unique-salt-value"; + var encrypted = _encryptionService.Encrypt("sensitive data", salt); + var decrypted = _encryptionService.Decrypt(encrypted, salt); + } +} +``` + +### DatabaseEnvironmentModelFactory + +Factory for creating DatabaseEnvironmentModel objects from connection strings. + +**Usage:** +```csharp +public class MyService +{ + private readonly IDatabaseEnvironmentModelFactory _factory; + + public MyService(IDatabaseEnvironmentModelFactory factory) + { + _factory = factory; + } + + public void DisplayDbInfo() + { + var connectionString = "Server=myserver;Database=mydb;..."; + var model = _factory.CreateFromConnectionString("MyDatabase", connectionString); + + Console.WriteLine($"Server: {model.ServerName}"); + Console.WriteLine($"Database: {model.DatabaseName}"); + } +} +``` + +### Timezones + +Static class containing constants for standard US timezone values. + +**Available Constants:** +- `Timezones.AlaskanStandardTime` +- `Timezones.AtlanticStandardTime` +- `Timezones.CentralStandardTime` +- `Timezones.EasternStandardTime` +- `Timezones.HawaiianStandardTime` +- `Timezones.PacificStandardTime` +- `Timezones.MountainStandardTime` + +**Usage:** +```csharp +var centralTime = TimeZoneInfo.FindSystemTimeZoneById(Timezones.CentralStandardTime); +var convertedTime = TimeZoneInfo.ConvertTime(DateTime.UtcNow, centralTime); +``` + +## Additional Information -Detailed information can be found in the XML Comment documentation for the objects, we are working to add to this document as well. +For more detailed information, please refer to the XML documentation comments in the source code. All public APIs are fully documented with parameter descriptions, return values, and usage examples. diff --git a/docs/EncryptionServices.md b/docs/EncryptionServices.md new file mode 100644 index 0000000..bf6cba2 --- /dev/null +++ b/docs/EncryptionServices.md @@ -0,0 +1,381 @@ +# Encryption Services + +ICG.NetCore.Utilities provides two AES encryption services for different use cases. + +## Overview + +Both services use AES (Advanced Encryption Standard) symmetric encryption but differ in how they manage keys: + +- **AesEncryptionService**: Uses pre-configured static key and IV values +- **AesDerivedKeyEncryptionService**: Derives keys from a passphrase and salt using PBKDF2 (RFC 2898) + +## AesEncryptionService + +Use this service when you have pre-generated encryption keys and want to use the same key/IV combination for all encryption operations. + +### Configuration + +```csharp +// In Startup.cs or Program.cs +services.Configure(options => +{ + options.Key = "your-base64-encoded-32-byte-key"; + options.IV = "your-base64-encoded-16-byte-iv"; +}); + +// Don't forget to register the utilities +services.UseIcgNetCoreUtilities(); +``` + +### Generating Keys + +You can use the included EncryptionKeyGenerator utility or generate keys programmatically: + +```csharp +using System.Security.Cryptography; + +using (var aes = Aes.Create()) +{ + aes.GenerateKey(); + aes.GenerateIV(); + + var key = Convert.ToBase64String(aes.Key); + var iv = Convert.ToBase64String(aes.IV); + + Console.WriteLine($"Key: {key}"); + Console.WriteLine($"IV: {iv}"); +} +``` + +### Usage + +```csharp +public class SecureDataService +{ + private readonly IAesEncryptionService _encryptionService; + + public SecureDataService(IAesEncryptionService encryptionService) + { + _encryptionService = encryptionService; + } + + public string EncryptSensitiveData(string plainText) + { + return _encryptionService.Encrypt(plainText); + } + + public string DecryptSensitiveData(string encryptedText) + { + return _encryptionService.Decrypt(encryptedText); + } +} +``` + +### Interface + +```csharp +public interface IAesEncryptionService +{ + string Encrypt(string plainTextInput); + string Decrypt(string encryptedInput); +} +``` + +### Use Cases + +- Encrypting configuration values +- Protecting API keys in storage +- Encrypting database connection strings +- Any scenario where the same key is used across the application + +### Security Considerations + +- Store keys securely (use Azure Key Vault, AWS Secrets Manager, etc.) +- Never commit keys to source control +- Rotate keys periodically +- Use strong random keys (256-bit recommended) + +## AesDerivedKeyEncryptionService + +Use this service when you want to derive encryption keys from a passphrase and salt. This is ideal for per-user or per-record encryption where each item can have a unique salt. + +### Configuration + +```csharp +// In Startup.cs or Program.cs +services.Configure(options => +{ + options.Passphrase = "your-secure-passphrase"; +}); + +// Don't forget to register the utilities +services.UseIcgNetCoreUtilities(); +``` + +### Usage with Configured Passphrase + +```csharp +public class UserDataService +{ + private readonly IAesDerivedKeyEncryptionService _encryptionService; + + public UserDataService(IAesDerivedKeyEncryptionService encryptionService) + { + _encryptionService = encryptionService; + } + + public void SaveUserData(int userId, string sensitiveData) + { + // Use user ID as salt for per-user encryption + var salt = $"user_{userId}"; + var encrypted = _encryptionService.Encrypt(sensitiveData, salt); + + // Save encrypted data to database + SaveToDatabase(userId, encrypted); + } + + public string LoadUserData(int userId) + { + var encrypted = LoadFromDatabase(userId); + var salt = $"user_{userId}"; + return _encryptionService.Decrypt(encrypted, salt); + } +} +``` + +### Usage with Custom Passphrase + +```csharp +public class DocumentEncryptionService +{ + private readonly IAesDerivedKeyEncryptionService _encryptionService; + + public DocumentEncryptionService(IAesDerivedKeyEncryptionService encryptionService) + { + _encryptionService = encryptionService; + } + + public EncryptedDocument EncryptDocument(string content, string userPassword) + { + // Generate a unique salt for this document + var salt = Guid.NewGuid().ToString(); + + // Use user's password as passphrase + var encrypted = _encryptionService.Encrypt(content, salt, userPassword); + + return new EncryptedDocument + { + EncryptedContent = encrypted, + Salt = salt // Store salt with the document + }; + } + + public string DecryptDocument(EncryptedDocument doc, string userPassword) + { + return _encryptionService.Decrypt(doc.EncryptedContent, doc.Salt, userPassword); + } +} +``` + +### Interface + +```csharp +public interface IAesDerivedKeyEncryptionService +{ + // Uses configured passphrase + string Encrypt(string plainTextInput, string salt); + string Decrypt(string encryptedInput, string salt); + + // Uses provided passphrase + string Encrypt(string plainTextInput, string salt, string passphrase); + string Decrypt(string encryptedInput, string salt, string passphrase); +} +``` + +### Use Cases + +- Per-user data encryption +- Per-record encryption in databases +- Password-protected documents or files +- Multi-tenant applications where each tenant needs isolated encryption +- Scenarios requiring key rotation without re-encrypting all data + +### Security Considerations + +- Use unique salts for each encryption operation +- Store salts alongside encrypted data (they don't need to be secret) +- Use strong passphrases (long and complex) +- Consider using user-provided passwords for password-protected features +- Salt values should be unique but don't need to be cryptographically random + +## Comparison + +| Feature | AesEncryptionService | AesDerivedKeyEncryptionService | +|---------|---------------------|--------------------------------| +| Key Management | Static pre-generated keys | Keys derived from passphrase + salt | +| Configuration | Requires base64 key and IV | Requires passphrase only | +| Per-item Keys | No | Yes (via unique salts) | +| Performance | Faster (no key derivation) | Slightly slower (key derivation overhead) | +| Use Case | Application-wide encryption | Per-user or per-record encryption | +| Key Rotation | Requires re-encrypting all data | Can use different passphrases per operation | + +## Best Practices + +### General + +1. **Never log or display encrypted values** in production +2. **Handle exceptions** from encryption/decryption operations gracefully +3. **Validate input** before encrypting to avoid wasting resources +4. **Use HTTPS** when transmitting encrypted data to prevent man-in-the-middle attacks + +### AesEncryptionService + +```csharp +// ✅ Good: Secure configuration +services.Configure(options => +{ + options.Key = Configuration["Encryption:Key"]; // From secure config + options.IV = Configuration["Encryption:IV"]; +}); + +// ❌ Bad: Hardcoded keys +services.Configure(options => +{ + options.Key = "hardcoded-key-in-source-code"; // DON'T DO THIS +}); +``` + +### AesDerivedKeyEncryptionService + +```csharp +// ✅ Good: Unique salt per record +public void EncryptUserData(User user) +{ + var salt = $"user_{user.Id}_{Guid.NewGuid()}"; + user.EncryptedData = _encryptionService.Encrypt(user.SensitiveData, salt); + user.Salt = salt; // Store with user +} + +// ❌ Bad: Reusing same salt +public void EncryptUserData(User user) +{ + var salt = "same-salt-for-everyone"; // DON'T DO THIS + user.EncryptedData = _encryptionService.Encrypt(user.SensitiveData, salt); +} +``` + +## Error Handling + +```csharp +public class SafeEncryptionService +{ + private readonly IAesEncryptionService _encryptionService; + private readonly ILogger _logger; + + public string SafeEncrypt(string plainText) + { + try + { + if (string.IsNullOrEmpty(plainText)) + return plainText; + + return _encryptionService.Encrypt(plainText); + } + catch (ArgumentNullException ex) + { + _logger.LogError(ex, "Null argument provided for encryption"); + throw; + } + catch (CryptographicException ex) + { + _logger.LogError(ex, "Encryption failed"); + throw new InvalidOperationException("Failed to encrypt data", ex); + } + } + + public string SafeDecrypt(string encryptedText) + { + try + { + if (string.IsNullOrEmpty(encryptedText)) + return encryptedText; + + return _encryptionService.Decrypt(encryptedText); + } + catch (ArgumentNullException ex) + { + _logger.LogError(ex, "Null argument provided for decryption"); + throw; + } + catch (FormatException ex) + { + _logger.LogError(ex, "Invalid encrypted data format"); + throw new InvalidOperationException("Failed to decrypt data: invalid format", ex); + } + catch (CryptographicException ex) + { + _logger.LogError(ex, "Decryption failed"); + throw new InvalidOperationException("Failed to decrypt data", ex); + } + } +} +``` + +## Testing + +### Unit Testing with Mock + +```csharp +[Test] +public void Test_EncryptDecrypt_RoundTrip() +{ + // Arrange + var options = Options.Create(new AesEncryptionServiceOptions + { + Key = "your-test-key-base64", + IV = "your-test-iv-base64" + }); + var service = new AesEncryptionService(options); + var plainText = "sensitive data"; + + // Act + var encrypted = service.Encrypt(plainText); + var decrypted = service.Decrypt(encrypted); + + // Assert + Assert.AreNotEqual(plainText, encrypted); + Assert.AreEqual(plainText, decrypted); +} +``` + +## Migration Between Services + +If you need to migrate from AesEncryptionService to AesDerivedKeyEncryptionService: + +```csharp +public class EncryptionMigrationService +{ + private readonly IAesEncryptionService _oldService; + private readonly IAesDerivedKeyEncryptionService _newService; + + public void MigrateUserData(User user) + { + // Decrypt with old service + var plainText = _oldService.Decrypt(user.EncryptedData); + + // Generate new salt + var salt = Guid.NewGuid().ToString(); + + // Encrypt with new service + user.EncryptedData = _newService.Encrypt(plainText, salt); + user.Salt = salt; + user.EncryptionVersion = 2; // Track encryption method + } +} +``` + +## Additional Resources + +- [AES Encryption (Wikipedia)](https://en.wikipedia.org/wiki/Advanced_Encryption_Standard) +- [PBKDF2 (RFC 2898)](https://tools.ietf.org/html/rfc2898) +- [.NET Cryptography Model](https://docs.microsoft.com/en-us/dotnet/standard/security/cryptography-model) diff --git a/docs/EnumExtensions.md b/docs/EnumExtensions.md new file mode 100644 index 0000000..d826ed0 --- /dev/null +++ b/docs/EnumExtensions.md @@ -0,0 +1,312 @@ +# EnumExtensions + +Extension methods for Enum types that help display formatted enum values using Display attributes. + +## Namespace + +```csharp +using ICG.NetCore.Utilities; +using System.ComponentModel.DataAnnotations; +``` + +## Overview + +The EnumExtensions class provides extension methods that make it easier to work with enum values and their Display attributes. This is particularly useful when you need to show user-friendly names for enum values in UI applications. + +## Methods + +### GetDisplayNameOrStringValue + +Returns the configured Display Name, or default string value for the given enum value. This is the safest method as it will never throw an exception. + +**Signature:** +```csharp +public static string GetDisplayNameOrStringValue(this Enum enumValue) +``` + +**Parameters:** +- `enumValue`: The enum value to get the display name for + +**Returns:** The Display attribute's Name property if it exists, otherwise the enum's ToString() value + +**Example:** +```csharp +public enum UserStatus +{ + [Display(Name = "Active User")] + Active, + + [Display(Name = "Temporarily Inactive")] + Inactive, + + Pending // No Display attribute +} + +var activeDisplay = UserStatus.Active.GetDisplayNameOrStringValue(); +// Returns: "Active User" + +var inactiveDisplay = UserStatus.Inactive.GetDisplayNameOrStringValue(); +// Returns: "Temporarily Inactive" + +var pendingDisplay = UserStatus.Pending.GetDisplayNameOrStringValue(); +// Returns: "Pending" +``` + +### GetDisplayName + +Gets the display name of an enum value. This method will throw a NullReferenceException if the Display attribute is not found. + +**Signature:** +```csharp +public static string GetDisplayName(this Enum enumValue) +``` + +**Parameters:** +- `enumValue`: The enum value to get the display name for + +**Returns:** The Display attribute's Name property + +**Throws:** `NullReferenceException` when the Display attribute is not found + +**Example:** +```csharp +public enum Priority +{ + [Display(Name = "Low Priority")] + Low, + + [Display(Name = "Medium Priority")] + Medium, + + [Display(Name = "High Priority")] + High +} + +var display = Priority.High.GetDisplayName(); +// Returns: "High Priority" + +// This would throw NullReferenceException: +// var display = SomeEnumWithoutDisplayAttribute.Value.GetDisplayName(); +``` + +**Use Case:** Use this method when you know all enum values have Display attributes and you want to fail fast if one is missing. + +### HasDisplayName + +Checks whether an enum value has a Display attribute. + +**Signature:** +```csharp +public static bool HasDisplayName(this Enum enumValue) +``` + +**Parameters:** +- `enumValue`: The enum value to check + +**Returns:** `true` if the enum value has a Display attribute; otherwise `false` + +**Example:** +```csharp +public enum OrderStatus +{ + [Display(Name = "Order Pending")] + Pending, + + Shipped // No Display attribute +} + +var hasPendingDisplay = OrderStatus.Pending.HasDisplayName(); +// Returns: true + +var hasShippedDisplay = OrderStatus.Shipped.HasDisplayName(); +// Returns: false + +// Conditional display logic: +var displayValue = OrderStatus.Shipped.HasDisplayName() + ? OrderStatus.Shipped.GetDisplayName() + : OrderStatus.Shipped.ToString(); +``` + +## Real-World Usage Scenarios + +### Displaying Enum Values in a Dropdown + +```csharp +// ASP.NET Core Razor View +@model IEnumerable + + +``` + +### API Response with Friendly Names + +```csharp +public class OrderDto +{ + public int Id { get; set; } + public string Status { get; set; } + public string StatusDisplay { get; set; } +} + +public OrderDto MapToDto(Order order) +{ + return new OrderDto + { + Id = order.Id, + Status = order.Status.ToString(), + StatusDisplay = order.Status.GetDisplayNameOrStringValue() + }; +} +``` + +### Building a Dynamic Table + +```csharp +public class EnumTableBuilder +{ + public List GetEnumDisplayItems() where TEnum : Enum + { + return Enum.GetValues() + .Select(value => new EnumDisplayItem + { + Value = Convert.ToInt32(value), + Name = value.ToString(), + DisplayName = value.GetDisplayNameOrStringValue(), + HasCustomDisplay = value.HasDisplayName() + }) + .ToList(); + } +} +``` + +### Validation Messages with Display Names + +```csharp +public class StatusValidator +{ + public string ValidateStatus(UserStatus status) + { + if (status == UserStatus.Inactive) + { + return $"Cannot process: Status is {status.GetDisplayNameOrStringValue()}"; + } + return "Valid"; + } +} +``` + +## Best Practices + +### When to Use Each Method + +1. **GetDisplayNameOrStringValue**: Use this in most cases, especially in UI code where you always need a value to display. It's the safest option and won't throw exceptions. + +2. **GetDisplayName**: Use this when you want to ensure all enum values have Display attributes and want to fail fast during development if one is missing. + +3. **HasDisplayName**: Use this when you need to conditionally handle enum values differently based on whether they have Display attributes. + +### Enum Definition Best Practices + +```csharp +// ✅ Good: Consistent Display attributes +public enum PaymentMethod +{ + [Display(Name = "Credit Card")] + CreditCard, + + [Display(Name = "PayPal")] + PayPal, + + [Display(Name = "Bank Transfer")] + BankTransfer +} + +// ⚠️ Caution: Inconsistent attributes (use GetDisplayNameOrStringValue) +public enum NotificationPreference +{ + [Display(Name = "Email Notifications")] + Email, + + Sms, // No Display attribute + + [Display(Name = "Push Notifications")] + Push +} +``` + +### Using with Localization + +The Display attribute also supports resource files for localization: + +```csharp +public enum Status +{ + [Display(Name = "ActiveStatus", ResourceType = typeof(Resources))] + Active, + + [Display(Name = "InactiveStatus", ResourceType = typeof(Resources))] + Inactive +} + +// The GetDisplayName() method will automatically use the resource file +var display = Status.Active.GetDisplayName(); +// Returns localized string from Resources.ActiveStatus +``` + +## Common Patterns + +### Creating a Helper Method for Dropdowns + +```csharp +public static class EnumHelper +{ + public static SelectList GetEnumSelectList() where TEnum : Enum + { + var items = Enum.GetValues() + .Select(e => new SelectListItem + { + Value = e.ToString(), + Text = e.GetDisplayNameOrStringValue() + }); + + return new SelectList(items, "Value", "Text"); + } +} + +// Usage in controller: +ViewBag.StatusList = EnumHelper.GetEnumSelectList(); +``` + +### Extension Method for All Enum Values + +```csharp +public static class EnumExtensionHelpers +{ + public static Dictionary GetAllDisplayNames() + where TEnum : Enum + { + return Enum.GetValues() + .Cast() + .ToDictionary(e => e, e => e.GetDisplayNameOrStringValue()); + } +} + +// Usage: +var statusDisplayNames = EnumExtensionHelpers.GetAllDisplayNames(); +``` + +## Requirements + +- Requires `System.ComponentModel.DataAnnotations` namespace for Display attribute +- Works with all .NET enum types +- Compatible with .NET Core 3.1+ and .NET 5+ + +## Related + +- [System.ComponentModel.DataAnnotations.DisplayAttribute](https://docs.microsoft.com/en-us/dotnet/api/system.componentmodel.dataannotations.displayattribute) +- [Enum Class](https://docs.microsoft.com/en-us/dotnet/api/system.enum) diff --git a/docs/QueryableExtensions.md b/docs/QueryableExtensions.md new file mode 100644 index 0000000..e0dec5c --- /dev/null +++ b/docs/QueryableExtensions.md @@ -0,0 +1,239 @@ +# QueryableExtensions + +Extension methods for `IQueryable` that provide conditional querying capabilities to simplify building dynamic queries. + +## Namespace + +```csharp +using ICG.NetCore.Utilities; +``` + +## Overview + +The QueryableExtensions class provides a set of extension methods that allow you to conditionally apply LINQ operations to IQueryable sequences. This is particularly useful when building dynamic queries where certain filters or operations should only be applied based on runtime conditions. + +## Methods + +### WhereIf + +Conditionally applies a filter to the query if the specified condition is true. + +**Signature:** +```csharp +public static IQueryable WhereIf( + this IQueryable source, + bool condition, + Expression> predicate) +``` + +**Parameters:** +- `source`: The source queryable sequence +- `condition`: If true, the predicate is applied; otherwise, the source is returned unchanged +- `predicate`: The filter expression to apply + +**Returns:** The filtered queryable sequence if condition is true; otherwise, the original sequence + +**Example:** +```csharp +var activeOnly = true; +var searchTerm = "John"; + +var users = dbContext.Users + .WhereIf(activeOnly, u => u.IsActive) + .WhereIf(!string.IsNullOrEmpty(searchTerm), u => u.Name.Contains(searchTerm)) + .ToList(); + +// This is cleaner than: +var query = dbContext.Users.AsQueryable(); +if (activeOnly) + query = query.Where(u => u.IsActive); +if (!string.IsNullOrEmpty(searchTerm)) + query = query.Where(u => u.Name.Contains(searchTerm)); +var users = query.ToList(); +``` + +### OrderByIf + +Conditionally applies an ascending order to the query if the specified condition is true. + +**Signature:** +```csharp +public static IQueryable OrderByIf( + this IQueryable query, + bool condition, + Expression> orderBy) +``` + +**Parameters:** +- `query`: The source queryable sequence +- `condition`: If true, the ordering is applied; otherwise, the source is returned unchanged +- `orderBy`: The key selector expression for ordering + +**Returns:** The ordered queryable sequence if condition is true; otherwise, the original sequence + +**Example:** +```csharp +var sortByName = true; + +var users = dbContext.Users + .WhereIf(activeOnly, u => u.IsActive) + .OrderByIf(sortByName, u => u.LastName) + .ToList(); +``` + +### OrderByDescendingIf + +Conditionally applies a descending order to the query if the specified condition is true. + +**Signature:** +```csharp +public static IQueryable OrderByDescendingIf( + this IQueryable query, + bool condition, + Expression> orderBy) +``` + +**Parameters:** +- `query`: The source queryable sequence +- `condition`: If true, the ordering is applied; otherwise, the source is returned unchanged +- `orderBy`: The key selector expression for ordering + +**Returns:** The ordered queryable sequence in descending order if condition is true; otherwise, the original sequence + +**Example:** +```csharp +var sortDescending = true; + +var users = dbContext.Users + .OrderByDescendingIf(sortDescending, u => u.CreatedDate) + .ToList(); +``` + +### GetPage + +Returns a specific page of results from the queryable sequence. + +**Signature:** +```csharp +public static IQueryable GetPage( + this IQueryable query, + int pageNumber, + int pageSize) +``` + +**Parameters:** +- `query`: The source queryable sequence +- `pageNumber`: The page number to retrieve (1-based) +- `pageSize`: The number of items per page + +**Returns:** A queryable sequence containing the specified page of results + +**Example:** +```csharp +var pageNumber = 2; +var pageSize = 10; + +var users = dbContext.Users + .OrderBy(u => u.LastName) + .GetPage(pageNumber, pageSize) + .ToList(); + +// This retrieves items 11-20 (page 2 with 10 items per page) +``` + +**Important Note:** The query should be ordered before calling GetPage to ensure consistent pagination results. + +### DistinctBy + +Returns distinct elements from a sequence by using a specified key selector. + +**Signature:** +```csharp +public static IQueryable DistinctBy( + this IQueryable query, + Expression> keySelector) +``` + +**Parameters:** +- `query`: The source queryable sequence +- `keySelector`: A function to extract the key for each element + +**Returns:** A queryable sequence that contains distinct elements based on the specified key + +**Example:** +```csharp +// Get users with unique email addresses +var uniqueUsers = dbContext.Users + .DistinctBy(u => u.Email) + .ToList(); + +// Get orders with unique customer IDs +var uniqueCustomerOrders = dbContext.Orders + .DistinctBy(o => o.CustomerId) + .ToList(); +``` + +## Real-World Usage Scenarios + +### Building a Search API + +```csharp +public class UserSearchService +{ + private readonly DbContext _context; + + public List SearchUsers(UserSearchCriteria criteria) + { + return _context.Users + .WhereIf(criteria.ActiveOnly, u => u.IsActive) + .WhereIf(!string.IsNullOrEmpty(criteria.SearchTerm), + u => u.FirstName.Contains(criteria.SearchTerm) || + u.LastName.Contains(criteria.SearchTerm)) + .WhereIf(criteria.MinAge.HasValue, u => u.Age >= criteria.MinAge.Value) + .WhereIf(criteria.MaxAge.HasValue, u => u.Age <= criteria.MaxAge.Value) + .OrderByIf(criteria.SortBy == "name", u => u.LastName) + .OrderByDescendingIf(criteria.SortBy == "date", u => u.CreatedDate) + .GetPage(criteria.PageNumber, criteria.PageSize) + .ToList(); + } +} +``` + +### Filtering with Optional Parameters + +```csharp +public IActionResult GetProducts( + string category = null, + decimal? minPrice = null, + decimal? maxPrice = null, + bool inStock = false, + int page = 1, + int pageSize = 20) +{ + var products = _context.Products + .WhereIf(!string.IsNullOrEmpty(category), p => p.Category == category) + .WhereIf(minPrice.HasValue, p => p.Price >= minPrice.Value) + .WhereIf(maxPrice.HasValue, p => p.Price <= maxPrice.Value) + .WhereIf(inStock, p => p.StockQuantity > 0) + .OrderBy(p => p.Name) + .GetPage(page, pageSize) + .ToList(); + + return Ok(products); +} +``` + +## Benefits + +1. **Cleaner Code**: Reduces the need for conditional if statements in query building +2. **Fluent Interface**: Maintains a fluent, chainable API style +3. **Better Readability**: Makes complex conditional queries more readable +4. **Type Safety**: Maintains full type safety with expression trees +5. **Query Optimization**: Conditions are evaluated before query execution, so only necessary predicates are sent to the database + +## Notes + +- All methods work with Entity Framework Core and any other IQueryable provider +- These extensions do not execute the query - they build the expression tree +- Remember to call `.ToList()`, `.ToArray()`, or similar to execute the query +- These are particularly useful in API endpoints where you need to build dynamic queries based on query string parameters diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..1986d1c --- /dev/null +++ b/docs/README.md @@ -0,0 +1,111 @@ +# ICG.NetCore.Utilities Documentation + +Welcome to the comprehensive documentation for ICG.NetCore.Utilities. + +## Quick Links + +- [Main README](../README.md) - Overview, installation, and quick reference +- [QueryableExtensions](QueryableExtensions.md) - IQueryable extension methods for conditional filtering, ordering, and paging +- [EnumExtensions](EnumExtensions.md) - Enum extension methods for working with Display attributes +- [Encryption Services](EncryptionServices.md) - AES encryption services documentation + +## What's Included + +### Extension Methods + +Extension methods that add functionality to existing .NET types: + +- **[QueryableExtensions](QueryableExtensions.md)** - Conditional LINQ operations + - WhereIf - Conditional filtering + - OrderByIf / OrderByDescendingIf - Conditional ordering + - GetPage - Pagination helper + - DistinctBy - Distinct by key selector + +- **[EnumExtensions](EnumExtensions.md)** - Display attribute helpers + - GetDisplayNameOrStringValue - Safe display name retrieval + - GetDisplayName - Display name retrieval (strict) + - HasDisplayName - Check for Display attribute + +- **IdentityExtensions** - Claims helper + - GetClaimValue - Extract claim values from IIdentity + +### Services + +Services that provide testable implementations and utility functionality: + +- **[Encryption Services](EncryptionServices.md)** + - IAesEncryptionService - Static key encryption + - IAesDerivedKeyEncryptionService - Passphrase-based encryption + +- **Provider Interfaces** (for unit testing) + - IDirectoryProvider - System.IO.Directory wrapper + - IFileProvider - System.IO.File wrapper + - IPathProvider - System.IO.Path wrapper + - IGuidProvider - System.Guid wrapper + - ITimeProvider - System.DateTime wrapper + - ITimeSpanProvider - System.TimeSpan wrapper + +- **Other Services** + - IUrlSlugGenerator - URL-friendly slug generation + - IDatabaseEnvironmentModelFactory - Connection string parsing + +### Constants + +- **Timezones** - Standard US timezone constants + +## Getting Started + +### Installation + +```bash +Install-Package ICG.NetCore.Utilities +``` + +### Basic Setup + +In your `Startup.cs` or `Program.cs`: + +```csharp +services.UseIcgNetCoreUtilities(); +``` + +### Basic Usage + +```csharp +using ICG.NetCore.Utilities; + +// Use QueryableExtensions +var users = dbContext.Users + .WhereIf(activeOnly, u => u.IsActive) + .OrderByIf(sortByName, u => u.Name) + .GetPage(pageNumber, pageSize) + .ToList(); + +// Use EnumExtensions +var displayName = myEnum.GetDisplayNameOrStringValue(); + +// Use IdentityExtensions +var email = User.Identity.GetClaimValue(ClaimTypes.Email); +``` + +## Documentation Organization + +- **Main README**: High-level overview and quick reference for all features +- **Detailed Docs**: In-depth documentation for specific features with examples and best practices + - [QueryableExtensions.md](QueryableExtensions.md) - Complete guide to IQueryable extensions + - [EnumExtensions.md](EnumExtensions.md) - Complete guide to Enum extensions + - [EncryptionServices.md](EncryptionServices.md) - Complete guide to encryption services + +## Contributing + +See [CONTRIBUTING.md](../CONTRIBUTING.md) for information on how to contribute to this project. + +## Support + +For issues, questions, or contributions, please visit the [GitHub repository](https://github.com/IowaComputerGurus/netcore.utilities). + +## Additional Resources + +- [Source Code](https://github.com/IowaComputerGurus/netcore.utilities) +- [NuGet Package](https://www.nuget.org/packages/ICG.NetCore.Utilities/) +- [Release Notes](https://github.com/IowaComputerGurus/netcore.utilities/releases) From 971e07b82f0e270e16d92467d33f0464b50ec08c Mon Sep 17 00:00:00 2001 From: Mitchel Sellers Date: Sat, 6 Dec 2025 00:40:53 -0600 Subject: [PATCH 3/3] Updated from improper AI documentation --- README.md | 93 ++++----- docs/EncryptionServices.md | 381 ------------------------------------ docs/EnumExtensions.md | 312 ----------------------------- docs/QueryableExtensions.md | 239 ---------------------- docs/README.md | 111 ----------- 5 files changed, 42 insertions(+), 1094 deletions(-) delete mode 100644 docs/EncryptionServices.md delete mode 100644 docs/EnumExtensions.md delete mode 100644 docs/QueryableExtensions.md delete mode 100644 docs/README.md diff --git a/README.md b/README.md index 90de23c..75a7395 100644 --- a/README.md +++ b/README.md @@ -14,32 +14,23 @@ ICG.NetCore.Utilities ![](https://img.shields.io/nuget/v/icg.netcore.utilities.s [![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=IowaComputerGurus_netcore.utilities&metric=security_rating)](https://sonarcloud.io/dashboard?id=IowaComputerGurus_netcore.utilities) [![Technical Debt](https://sonarcloud.io/api/project_badges/measure?project=IowaComputerGurus_netcore.utilities&metric=sqale_index)](https://sonarcloud.io/dashboard?id=IowaComputerGurus_netcore.utilities) -## Documentation - -📚 **[View Detailed Documentation](docs/README.md)** - Comprehensive guides with examples and best practices - -Quick links: -- [QueryableExtensions](docs/QueryableExtensions.md) - Conditional LINQ operations -- [EnumExtensions](docs/EnumExtensions.md) - Display attribute helpers -- [Encryption Services](docs/EncryptionServices.md) - AES encryption services - ## Usage ### Installation Install from NuGet -``` +```` Install-Package ICG.NetCore.Utilities -``` +```` ### Register Dependencies -Inside of of your project's Startus.cs within the RegisterServices method add this line of code. +Inside of of your project's Startup.cs within the RegisterServices method add this line of code. -``` +```` services.UseIcgNetCoreUtilities(); -``` +```` ### Included C# Objects and Services @@ -86,36 +77,36 @@ Extension methods for `IQueryable` that provide conditional querying capabili **Available Methods:** - **WhereIf** - Conditionally applies a filter to the query - ```csharp + `````csharp var results = dbContext.Users .WhereIf(filterByActive, u => u.IsActive) .WhereIf(!string.IsNullOrEmpty(searchTerm), u => u.Name.Contains(searchTerm)); - ``` + ```` - **OrderByIf** - Conditionally applies ascending order to the query - ```csharp + ````csharp var results = dbContext.Users .OrderByIf(sortByName, u => u.Name); - ``` + ```` - **OrderByDescendingIf** - Conditionally applies descending order to the query - ```csharp + ````csharp var results = dbContext.Users .OrderByDescendingIf(sortByNewest, u => u.CreatedDate); - ``` + ```` - **GetPage** - Returns a specific page of results (1-based page numbers) - ```csharp + ````csharp var results = dbContext.Users .OrderBy(u => u.Name) .GetPage(pageNumber: 2, pageSize: 10); - ``` + ```` - **DistinctBy** - Returns distinct elements by a specified key selector - ```csharp + ````csharp var uniqueUsers = dbContext.Users .DistinctBy(u => u.Email); - ``` + ```` ### EnumExtensions @@ -124,7 +115,7 @@ Extension methods for working with Enum types and their Display attributes. **Available Methods:** - **GetDisplayNameOrStringValue** - Returns the Display Name attribute value or the enum's string value - ```csharp + ````csharp public enum Status { [Display(Name = "Active User")] @@ -134,18 +125,18 @@ Extension methods for working with Enum types and their Display attributes. var displayName = Status.Active.GetDisplayNameOrStringValue(); // "Active User" var defaultName = Status.Inactive.GetDisplayNameOrStringValue(); // "Inactive" - ``` + ```` - **GetDisplayName** - Gets the Display Name attribute value (throws if not found) - ```csharp + ````csharp var displayName = Status.Active.GetDisplayName(); // "Active User" - ``` + ```` - **HasDisplayName** - Checks if an enum value has a Display attribute - ```csharp + ````csharp var hasDisplay = Status.Active.HasDisplayName(); // true var hasDisplay = Status.Inactive.HasDisplayName(); // false - ``` + ```` ### IdentityExtensions @@ -154,24 +145,25 @@ Extension methods for working with IIdentity and claims. **Available Methods:** - **GetClaimValue** - Extracts the value of a specific claim type from a ClaimsIdentity - ```csharp + ````csharp // In a Razor view or controller var firstName = User.Identity.GetClaimValue("Profile:FirstName"); var email = User.Identity.GetClaimValue(ClaimTypes.Email); - ``` + ```` ### AesEncryptionService Service for AES symmetric encryption with pre-configured key and IV values. **Configuration:** -```csharp -services.Configure(options => -{ - options.Key = "your-base64-encoded-key"; - options.IV = "your-base64-encoded-iv"; -}); -``` +Add the following to your `appsettings.json`, Environment Variables, or any other configuration source to your application + +````json +"AesEncryptionServiceOptions": { + "IV" : "VALUE", + "Key" : "Value" +} +```` **Usage:** ```csharp @@ -197,15 +189,14 @@ public class MyService Service for AES encryption using derived keys from a passphrase and salt (using Rfc2898DeriveBytes). **Configuration:** -```csharp -services.Configure(options => -{ - options.Passphrase = "your-secure-passphrase"; -}); -``` +````json +"AesDerivedKeyEncryptionServiceOptions" : { + "Passphrase": "YourPassphrase" +} +```` **Usage:** -```csharp +````csharp public class MyService { private readonly IAesDerivedKeyEncryptionService _encryptionService; @@ -222,14 +213,14 @@ public class MyService var decrypted = _encryptionService.Decrypt(encrypted, salt); } } -``` +```` ### DatabaseEnvironmentModelFactory Factory for creating DatabaseEnvironmentModel objects from connection strings. **Usage:** -```csharp +````csharp public class MyService { private readonly IDatabaseEnvironmentModelFactory _factory; @@ -248,7 +239,7 @@ public class MyService Console.WriteLine($"Database: {model.DatabaseName}"); } } -``` +```` ### Timezones @@ -264,10 +255,10 @@ Static class containing constants for standard US timezone values. - `Timezones.MountainStandardTime` **Usage:** -```csharp +````csharp var centralTime = TimeZoneInfo.FindSystemTimeZoneById(Timezones.CentralStandardTime); var convertedTime = TimeZoneInfo.ConvertTime(DateTime.UtcNow, centralTime); -``` +```` ## Additional Information diff --git a/docs/EncryptionServices.md b/docs/EncryptionServices.md deleted file mode 100644 index bf6cba2..0000000 --- a/docs/EncryptionServices.md +++ /dev/null @@ -1,381 +0,0 @@ -# Encryption Services - -ICG.NetCore.Utilities provides two AES encryption services for different use cases. - -## Overview - -Both services use AES (Advanced Encryption Standard) symmetric encryption but differ in how they manage keys: - -- **AesEncryptionService**: Uses pre-configured static key and IV values -- **AesDerivedKeyEncryptionService**: Derives keys from a passphrase and salt using PBKDF2 (RFC 2898) - -## AesEncryptionService - -Use this service when you have pre-generated encryption keys and want to use the same key/IV combination for all encryption operations. - -### Configuration - -```csharp -// In Startup.cs or Program.cs -services.Configure(options => -{ - options.Key = "your-base64-encoded-32-byte-key"; - options.IV = "your-base64-encoded-16-byte-iv"; -}); - -// Don't forget to register the utilities -services.UseIcgNetCoreUtilities(); -``` - -### Generating Keys - -You can use the included EncryptionKeyGenerator utility or generate keys programmatically: - -```csharp -using System.Security.Cryptography; - -using (var aes = Aes.Create()) -{ - aes.GenerateKey(); - aes.GenerateIV(); - - var key = Convert.ToBase64String(aes.Key); - var iv = Convert.ToBase64String(aes.IV); - - Console.WriteLine($"Key: {key}"); - Console.WriteLine($"IV: {iv}"); -} -``` - -### Usage - -```csharp -public class SecureDataService -{ - private readonly IAesEncryptionService _encryptionService; - - public SecureDataService(IAesEncryptionService encryptionService) - { - _encryptionService = encryptionService; - } - - public string EncryptSensitiveData(string plainText) - { - return _encryptionService.Encrypt(plainText); - } - - public string DecryptSensitiveData(string encryptedText) - { - return _encryptionService.Decrypt(encryptedText); - } -} -``` - -### Interface - -```csharp -public interface IAesEncryptionService -{ - string Encrypt(string plainTextInput); - string Decrypt(string encryptedInput); -} -``` - -### Use Cases - -- Encrypting configuration values -- Protecting API keys in storage -- Encrypting database connection strings -- Any scenario where the same key is used across the application - -### Security Considerations - -- Store keys securely (use Azure Key Vault, AWS Secrets Manager, etc.) -- Never commit keys to source control -- Rotate keys periodically -- Use strong random keys (256-bit recommended) - -## AesDerivedKeyEncryptionService - -Use this service when you want to derive encryption keys from a passphrase and salt. This is ideal for per-user or per-record encryption where each item can have a unique salt. - -### Configuration - -```csharp -// In Startup.cs or Program.cs -services.Configure(options => -{ - options.Passphrase = "your-secure-passphrase"; -}); - -// Don't forget to register the utilities -services.UseIcgNetCoreUtilities(); -``` - -### Usage with Configured Passphrase - -```csharp -public class UserDataService -{ - private readonly IAesDerivedKeyEncryptionService _encryptionService; - - public UserDataService(IAesDerivedKeyEncryptionService encryptionService) - { - _encryptionService = encryptionService; - } - - public void SaveUserData(int userId, string sensitiveData) - { - // Use user ID as salt for per-user encryption - var salt = $"user_{userId}"; - var encrypted = _encryptionService.Encrypt(sensitiveData, salt); - - // Save encrypted data to database - SaveToDatabase(userId, encrypted); - } - - public string LoadUserData(int userId) - { - var encrypted = LoadFromDatabase(userId); - var salt = $"user_{userId}"; - return _encryptionService.Decrypt(encrypted, salt); - } -} -``` - -### Usage with Custom Passphrase - -```csharp -public class DocumentEncryptionService -{ - private readonly IAesDerivedKeyEncryptionService _encryptionService; - - public DocumentEncryptionService(IAesDerivedKeyEncryptionService encryptionService) - { - _encryptionService = encryptionService; - } - - public EncryptedDocument EncryptDocument(string content, string userPassword) - { - // Generate a unique salt for this document - var salt = Guid.NewGuid().ToString(); - - // Use user's password as passphrase - var encrypted = _encryptionService.Encrypt(content, salt, userPassword); - - return new EncryptedDocument - { - EncryptedContent = encrypted, - Salt = salt // Store salt with the document - }; - } - - public string DecryptDocument(EncryptedDocument doc, string userPassword) - { - return _encryptionService.Decrypt(doc.EncryptedContent, doc.Salt, userPassword); - } -} -``` - -### Interface - -```csharp -public interface IAesDerivedKeyEncryptionService -{ - // Uses configured passphrase - string Encrypt(string plainTextInput, string salt); - string Decrypt(string encryptedInput, string salt); - - // Uses provided passphrase - string Encrypt(string plainTextInput, string salt, string passphrase); - string Decrypt(string encryptedInput, string salt, string passphrase); -} -``` - -### Use Cases - -- Per-user data encryption -- Per-record encryption in databases -- Password-protected documents or files -- Multi-tenant applications where each tenant needs isolated encryption -- Scenarios requiring key rotation without re-encrypting all data - -### Security Considerations - -- Use unique salts for each encryption operation -- Store salts alongside encrypted data (they don't need to be secret) -- Use strong passphrases (long and complex) -- Consider using user-provided passwords for password-protected features -- Salt values should be unique but don't need to be cryptographically random - -## Comparison - -| Feature | AesEncryptionService | AesDerivedKeyEncryptionService | -|---------|---------------------|--------------------------------| -| Key Management | Static pre-generated keys | Keys derived from passphrase + salt | -| Configuration | Requires base64 key and IV | Requires passphrase only | -| Per-item Keys | No | Yes (via unique salts) | -| Performance | Faster (no key derivation) | Slightly slower (key derivation overhead) | -| Use Case | Application-wide encryption | Per-user or per-record encryption | -| Key Rotation | Requires re-encrypting all data | Can use different passphrases per operation | - -## Best Practices - -### General - -1. **Never log or display encrypted values** in production -2. **Handle exceptions** from encryption/decryption operations gracefully -3. **Validate input** before encrypting to avoid wasting resources -4. **Use HTTPS** when transmitting encrypted data to prevent man-in-the-middle attacks - -### AesEncryptionService - -```csharp -// ✅ Good: Secure configuration -services.Configure(options => -{ - options.Key = Configuration["Encryption:Key"]; // From secure config - options.IV = Configuration["Encryption:IV"]; -}); - -// ❌ Bad: Hardcoded keys -services.Configure(options => -{ - options.Key = "hardcoded-key-in-source-code"; // DON'T DO THIS -}); -``` - -### AesDerivedKeyEncryptionService - -```csharp -// ✅ Good: Unique salt per record -public void EncryptUserData(User user) -{ - var salt = $"user_{user.Id}_{Guid.NewGuid()}"; - user.EncryptedData = _encryptionService.Encrypt(user.SensitiveData, salt); - user.Salt = salt; // Store with user -} - -// ❌ Bad: Reusing same salt -public void EncryptUserData(User user) -{ - var salt = "same-salt-for-everyone"; // DON'T DO THIS - user.EncryptedData = _encryptionService.Encrypt(user.SensitiveData, salt); -} -``` - -## Error Handling - -```csharp -public class SafeEncryptionService -{ - private readonly IAesEncryptionService _encryptionService; - private readonly ILogger _logger; - - public string SafeEncrypt(string plainText) - { - try - { - if (string.IsNullOrEmpty(plainText)) - return plainText; - - return _encryptionService.Encrypt(plainText); - } - catch (ArgumentNullException ex) - { - _logger.LogError(ex, "Null argument provided for encryption"); - throw; - } - catch (CryptographicException ex) - { - _logger.LogError(ex, "Encryption failed"); - throw new InvalidOperationException("Failed to encrypt data", ex); - } - } - - public string SafeDecrypt(string encryptedText) - { - try - { - if (string.IsNullOrEmpty(encryptedText)) - return encryptedText; - - return _encryptionService.Decrypt(encryptedText); - } - catch (ArgumentNullException ex) - { - _logger.LogError(ex, "Null argument provided for decryption"); - throw; - } - catch (FormatException ex) - { - _logger.LogError(ex, "Invalid encrypted data format"); - throw new InvalidOperationException("Failed to decrypt data: invalid format", ex); - } - catch (CryptographicException ex) - { - _logger.LogError(ex, "Decryption failed"); - throw new InvalidOperationException("Failed to decrypt data", ex); - } - } -} -``` - -## Testing - -### Unit Testing with Mock - -```csharp -[Test] -public void Test_EncryptDecrypt_RoundTrip() -{ - // Arrange - var options = Options.Create(new AesEncryptionServiceOptions - { - Key = "your-test-key-base64", - IV = "your-test-iv-base64" - }); - var service = new AesEncryptionService(options); - var plainText = "sensitive data"; - - // Act - var encrypted = service.Encrypt(plainText); - var decrypted = service.Decrypt(encrypted); - - // Assert - Assert.AreNotEqual(plainText, encrypted); - Assert.AreEqual(plainText, decrypted); -} -``` - -## Migration Between Services - -If you need to migrate from AesEncryptionService to AesDerivedKeyEncryptionService: - -```csharp -public class EncryptionMigrationService -{ - private readonly IAesEncryptionService _oldService; - private readonly IAesDerivedKeyEncryptionService _newService; - - public void MigrateUserData(User user) - { - // Decrypt with old service - var plainText = _oldService.Decrypt(user.EncryptedData); - - // Generate new salt - var salt = Guid.NewGuid().ToString(); - - // Encrypt with new service - user.EncryptedData = _newService.Encrypt(plainText, salt); - user.Salt = salt; - user.EncryptionVersion = 2; // Track encryption method - } -} -``` - -## Additional Resources - -- [AES Encryption (Wikipedia)](https://en.wikipedia.org/wiki/Advanced_Encryption_Standard) -- [PBKDF2 (RFC 2898)](https://tools.ietf.org/html/rfc2898) -- [.NET Cryptography Model](https://docs.microsoft.com/en-us/dotnet/standard/security/cryptography-model) diff --git a/docs/EnumExtensions.md b/docs/EnumExtensions.md deleted file mode 100644 index d826ed0..0000000 --- a/docs/EnumExtensions.md +++ /dev/null @@ -1,312 +0,0 @@ -# EnumExtensions - -Extension methods for Enum types that help display formatted enum values using Display attributes. - -## Namespace - -```csharp -using ICG.NetCore.Utilities; -using System.ComponentModel.DataAnnotations; -``` - -## Overview - -The EnumExtensions class provides extension methods that make it easier to work with enum values and their Display attributes. This is particularly useful when you need to show user-friendly names for enum values in UI applications. - -## Methods - -### GetDisplayNameOrStringValue - -Returns the configured Display Name, or default string value for the given enum value. This is the safest method as it will never throw an exception. - -**Signature:** -```csharp -public static string GetDisplayNameOrStringValue(this Enum enumValue) -``` - -**Parameters:** -- `enumValue`: The enum value to get the display name for - -**Returns:** The Display attribute's Name property if it exists, otherwise the enum's ToString() value - -**Example:** -```csharp -public enum UserStatus -{ - [Display(Name = "Active User")] - Active, - - [Display(Name = "Temporarily Inactive")] - Inactive, - - Pending // No Display attribute -} - -var activeDisplay = UserStatus.Active.GetDisplayNameOrStringValue(); -// Returns: "Active User" - -var inactiveDisplay = UserStatus.Inactive.GetDisplayNameOrStringValue(); -// Returns: "Temporarily Inactive" - -var pendingDisplay = UserStatus.Pending.GetDisplayNameOrStringValue(); -// Returns: "Pending" -``` - -### GetDisplayName - -Gets the display name of an enum value. This method will throw a NullReferenceException if the Display attribute is not found. - -**Signature:** -```csharp -public static string GetDisplayName(this Enum enumValue) -``` - -**Parameters:** -- `enumValue`: The enum value to get the display name for - -**Returns:** The Display attribute's Name property - -**Throws:** `NullReferenceException` when the Display attribute is not found - -**Example:** -```csharp -public enum Priority -{ - [Display(Name = "Low Priority")] - Low, - - [Display(Name = "Medium Priority")] - Medium, - - [Display(Name = "High Priority")] - High -} - -var display = Priority.High.GetDisplayName(); -// Returns: "High Priority" - -// This would throw NullReferenceException: -// var display = SomeEnumWithoutDisplayAttribute.Value.GetDisplayName(); -``` - -**Use Case:** Use this method when you know all enum values have Display attributes and you want to fail fast if one is missing. - -### HasDisplayName - -Checks whether an enum value has a Display attribute. - -**Signature:** -```csharp -public static bool HasDisplayName(this Enum enumValue) -``` - -**Parameters:** -- `enumValue`: The enum value to check - -**Returns:** `true` if the enum value has a Display attribute; otherwise `false` - -**Example:** -```csharp -public enum OrderStatus -{ - [Display(Name = "Order Pending")] - Pending, - - Shipped // No Display attribute -} - -var hasPendingDisplay = OrderStatus.Pending.HasDisplayName(); -// Returns: true - -var hasShippedDisplay = OrderStatus.Shipped.HasDisplayName(); -// Returns: false - -// Conditional display logic: -var displayValue = OrderStatus.Shipped.HasDisplayName() - ? OrderStatus.Shipped.GetDisplayName() - : OrderStatus.Shipped.ToString(); -``` - -## Real-World Usage Scenarios - -### Displaying Enum Values in a Dropdown - -```csharp -// ASP.NET Core Razor View -@model IEnumerable - - -``` - -### API Response with Friendly Names - -```csharp -public class OrderDto -{ - public int Id { get; set; } - public string Status { get; set; } - public string StatusDisplay { get; set; } -} - -public OrderDto MapToDto(Order order) -{ - return new OrderDto - { - Id = order.Id, - Status = order.Status.ToString(), - StatusDisplay = order.Status.GetDisplayNameOrStringValue() - }; -} -``` - -### Building a Dynamic Table - -```csharp -public class EnumTableBuilder -{ - public List GetEnumDisplayItems() where TEnum : Enum - { - return Enum.GetValues() - .Select(value => new EnumDisplayItem - { - Value = Convert.ToInt32(value), - Name = value.ToString(), - DisplayName = value.GetDisplayNameOrStringValue(), - HasCustomDisplay = value.HasDisplayName() - }) - .ToList(); - } -} -``` - -### Validation Messages with Display Names - -```csharp -public class StatusValidator -{ - public string ValidateStatus(UserStatus status) - { - if (status == UserStatus.Inactive) - { - return $"Cannot process: Status is {status.GetDisplayNameOrStringValue()}"; - } - return "Valid"; - } -} -``` - -## Best Practices - -### When to Use Each Method - -1. **GetDisplayNameOrStringValue**: Use this in most cases, especially in UI code where you always need a value to display. It's the safest option and won't throw exceptions. - -2. **GetDisplayName**: Use this when you want to ensure all enum values have Display attributes and want to fail fast during development if one is missing. - -3. **HasDisplayName**: Use this when you need to conditionally handle enum values differently based on whether they have Display attributes. - -### Enum Definition Best Practices - -```csharp -// ✅ Good: Consistent Display attributes -public enum PaymentMethod -{ - [Display(Name = "Credit Card")] - CreditCard, - - [Display(Name = "PayPal")] - PayPal, - - [Display(Name = "Bank Transfer")] - BankTransfer -} - -// ⚠️ Caution: Inconsistent attributes (use GetDisplayNameOrStringValue) -public enum NotificationPreference -{ - [Display(Name = "Email Notifications")] - Email, - - Sms, // No Display attribute - - [Display(Name = "Push Notifications")] - Push -} -``` - -### Using with Localization - -The Display attribute also supports resource files for localization: - -```csharp -public enum Status -{ - [Display(Name = "ActiveStatus", ResourceType = typeof(Resources))] - Active, - - [Display(Name = "InactiveStatus", ResourceType = typeof(Resources))] - Inactive -} - -// The GetDisplayName() method will automatically use the resource file -var display = Status.Active.GetDisplayName(); -// Returns localized string from Resources.ActiveStatus -``` - -## Common Patterns - -### Creating a Helper Method for Dropdowns - -```csharp -public static class EnumHelper -{ - public static SelectList GetEnumSelectList() where TEnum : Enum - { - var items = Enum.GetValues() - .Select(e => new SelectListItem - { - Value = e.ToString(), - Text = e.GetDisplayNameOrStringValue() - }); - - return new SelectList(items, "Value", "Text"); - } -} - -// Usage in controller: -ViewBag.StatusList = EnumHelper.GetEnumSelectList(); -``` - -### Extension Method for All Enum Values - -```csharp -public static class EnumExtensionHelpers -{ - public static Dictionary GetAllDisplayNames() - where TEnum : Enum - { - return Enum.GetValues() - .Cast() - .ToDictionary(e => e, e => e.GetDisplayNameOrStringValue()); - } -} - -// Usage: -var statusDisplayNames = EnumExtensionHelpers.GetAllDisplayNames(); -``` - -## Requirements - -- Requires `System.ComponentModel.DataAnnotations` namespace for Display attribute -- Works with all .NET enum types -- Compatible with .NET Core 3.1+ and .NET 5+ - -## Related - -- [System.ComponentModel.DataAnnotations.DisplayAttribute](https://docs.microsoft.com/en-us/dotnet/api/system.componentmodel.dataannotations.displayattribute) -- [Enum Class](https://docs.microsoft.com/en-us/dotnet/api/system.enum) diff --git a/docs/QueryableExtensions.md b/docs/QueryableExtensions.md deleted file mode 100644 index e0dec5c..0000000 --- a/docs/QueryableExtensions.md +++ /dev/null @@ -1,239 +0,0 @@ -# QueryableExtensions - -Extension methods for `IQueryable` that provide conditional querying capabilities to simplify building dynamic queries. - -## Namespace - -```csharp -using ICG.NetCore.Utilities; -``` - -## Overview - -The QueryableExtensions class provides a set of extension methods that allow you to conditionally apply LINQ operations to IQueryable sequences. This is particularly useful when building dynamic queries where certain filters or operations should only be applied based on runtime conditions. - -## Methods - -### WhereIf - -Conditionally applies a filter to the query if the specified condition is true. - -**Signature:** -```csharp -public static IQueryable WhereIf( - this IQueryable source, - bool condition, - Expression> predicate) -``` - -**Parameters:** -- `source`: The source queryable sequence -- `condition`: If true, the predicate is applied; otherwise, the source is returned unchanged -- `predicate`: The filter expression to apply - -**Returns:** The filtered queryable sequence if condition is true; otherwise, the original sequence - -**Example:** -```csharp -var activeOnly = true; -var searchTerm = "John"; - -var users = dbContext.Users - .WhereIf(activeOnly, u => u.IsActive) - .WhereIf(!string.IsNullOrEmpty(searchTerm), u => u.Name.Contains(searchTerm)) - .ToList(); - -// This is cleaner than: -var query = dbContext.Users.AsQueryable(); -if (activeOnly) - query = query.Where(u => u.IsActive); -if (!string.IsNullOrEmpty(searchTerm)) - query = query.Where(u => u.Name.Contains(searchTerm)); -var users = query.ToList(); -``` - -### OrderByIf - -Conditionally applies an ascending order to the query if the specified condition is true. - -**Signature:** -```csharp -public static IQueryable OrderByIf( - this IQueryable query, - bool condition, - Expression> orderBy) -``` - -**Parameters:** -- `query`: The source queryable sequence -- `condition`: If true, the ordering is applied; otherwise, the source is returned unchanged -- `orderBy`: The key selector expression for ordering - -**Returns:** The ordered queryable sequence if condition is true; otherwise, the original sequence - -**Example:** -```csharp -var sortByName = true; - -var users = dbContext.Users - .WhereIf(activeOnly, u => u.IsActive) - .OrderByIf(sortByName, u => u.LastName) - .ToList(); -``` - -### OrderByDescendingIf - -Conditionally applies a descending order to the query if the specified condition is true. - -**Signature:** -```csharp -public static IQueryable OrderByDescendingIf( - this IQueryable query, - bool condition, - Expression> orderBy) -``` - -**Parameters:** -- `query`: The source queryable sequence -- `condition`: If true, the ordering is applied; otherwise, the source is returned unchanged -- `orderBy`: The key selector expression for ordering - -**Returns:** The ordered queryable sequence in descending order if condition is true; otherwise, the original sequence - -**Example:** -```csharp -var sortDescending = true; - -var users = dbContext.Users - .OrderByDescendingIf(sortDescending, u => u.CreatedDate) - .ToList(); -``` - -### GetPage - -Returns a specific page of results from the queryable sequence. - -**Signature:** -```csharp -public static IQueryable GetPage( - this IQueryable query, - int pageNumber, - int pageSize) -``` - -**Parameters:** -- `query`: The source queryable sequence -- `pageNumber`: The page number to retrieve (1-based) -- `pageSize`: The number of items per page - -**Returns:** A queryable sequence containing the specified page of results - -**Example:** -```csharp -var pageNumber = 2; -var pageSize = 10; - -var users = dbContext.Users - .OrderBy(u => u.LastName) - .GetPage(pageNumber, pageSize) - .ToList(); - -// This retrieves items 11-20 (page 2 with 10 items per page) -``` - -**Important Note:** The query should be ordered before calling GetPage to ensure consistent pagination results. - -### DistinctBy - -Returns distinct elements from a sequence by using a specified key selector. - -**Signature:** -```csharp -public static IQueryable DistinctBy( - this IQueryable query, - Expression> keySelector) -``` - -**Parameters:** -- `query`: The source queryable sequence -- `keySelector`: A function to extract the key for each element - -**Returns:** A queryable sequence that contains distinct elements based on the specified key - -**Example:** -```csharp -// Get users with unique email addresses -var uniqueUsers = dbContext.Users - .DistinctBy(u => u.Email) - .ToList(); - -// Get orders with unique customer IDs -var uniqueCustomerOrders = dbContext.Orders - .DistinctBy(o => o.CustomerId) - .ToList(); -``` - -## Real-World Usage Scenarios - -### Building a Search API - -```csharp -public class UserSearchService -{ - private readonly DbContext _context; - - public List SearchUsers(UserSearchCriteria criteria) - { - return _context.Users - .WhereIf(criteria.ActiveOnly, u => u.IsActive) - .WhereIf(!string.IsNullOrEmpty(criteria.SearchTerm), - u => u.FirstName.Contains(criteria.SearchTerm) || - u.LastName.Contains(criteria.SearchTerm)) - .WhereIf(criteria.MinAge.HasValue, u => u.Age >= criteria.MinAge.Value) - .WhereIf(criteria.MaxAge.HasValue, u => u.Age <= criteria.MaxAge.Value) - .OrderByIf(criteria.SortBy == "name", u => u.LastName) - .OrderByDescendingIf(criteria.SortBy == "date", u => u.CreatedDate) - .GetPage(criteria.PageNumber, criteria.PageSize) - .ToList(); - } -} -``` - -### Filtering with Optional Parameters - -```csharp -public IActionResult GetProducts( - string category = null, - decimal? minPrice = null, - decimal? maxPrice = null, - bool inStock = false, - int page = 1, - int pageSize = 20) -{ - var products = _context.Products - .WhereIf(!string.IsNullOrEmpty(category), p => p.Category == category) - .WhereIf(minPrice.HasValue, p => p.Price >= minPrice.Value) - .WhereIf(maxPrice.HasValue, p => p.Price <= maxPrice.Value) - .WhereIf(inStock, p => p.StockQuantity > 0) - .OrderBy(p => p.Name) - .GetPage(page, pageSize) - .ToList(); - - return Ok(products); -} -``` - -## Benefits - -1. **Cleaner Code**: Reduces the need for conditional if statements in query building -2. **Fluent Interface**: Maintains a fluent, chainable API style -3. **Better Readability**: Makes complex conditional queries more readable -4. **Type Safety**: Maintains full type safety with expression trees -5. **Query Optimization**: Conditions are evaluated before query execution, so only necessary predicates are sent to the database - -## Notes - -- All methods work with Entity Framework Core and any other IQueryable provider -- These extensions do not execute the query - they build the expression tree -- Remember to call `.ToList()`, `.ToArray()`, or similar to execute the query -- These are particularly useful in API endpoints where you need to build dynamic queries based on query string parameters diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index 1986d1c..0000000 --- a/docs/README.md +++ /dev/null @@ -1,111 +0,0 @@ -# ICG.NetCore.Utilities Documentation - -Welcome to the comprehensive documentation for ICG.NetCore.Utilities. - -## Quick Links - -- [Main README](../README.md) - Overview, installation, and quick reference -- [QueryableExtensions](QueryableExtensions.md) - IQueryable extension methods for conditional filtering, ordering, and paging -- [EnumExtensions](EnumExtensions.md) - Enum extension methods for working with Display attributes -- [Encryption Services](EncryptionServices.md) - AES encryption services documentation - -## What's Included - -### Extension Methods - -Extension methods that add functionality to existing .NET types: - -- **[QueryableExtensions](QueryableExtensions.md)** - Conditional LINQ operations - - WhereIf - Conditional filtering - - OrderByIf / OrderByDescendingIf - Conditional ordering - - GetPage - Pagination helper - - DistinctBy - Distinct by key selector - -- **[EnumExtensions](EnumExtensions.md)** - Display attribute helpers - - GetDisplayNameOrStringValue - Safe display name retrieval - - GetDisplayName - Display name retrieval (strict) - - HasDisplayName - Check for Display attribute - -- **IdentityExtensions** - Claims helper - - GetClaimValue - Extract claim values from IIdentity - -### Services - -Services that provide testable implementations and utility functionality: - -- **[Encryption Services](EncryptionServices.md)** - - IAesEncryptionService - Static key encryption - - IAesDerivedKeyEncryptionService - Passphrase-based encryption - -- **Provider Interfaces** (for unit testing) - - IDirectoryProvider - System.IO.Directory wrapper - - IFileProvider - System.IO.File wrapper - - IPathProvider - System.IO.Path wrapper - - IGuidProvider - System.Guid wrapper - - ITimeProvider - System.DateTime wrapper - - ITimeSpanProvider - System.TimeSpan wrapper - -- **Other Services** - - IUrlSlugGenerator - URL-friendly slug generation - - IDatabaseEnvironmentModelFactory - Connection string parsing - -### Constants - -- **Timezones** - Standard US timezone constants - -## Getting Started - -### Installation - -```bash -Install-Package ICG.NetCore.Utilities -``` - -### Basic Setup - -In your `Startup.cs` or `Program.cs`: - -```csharp -services.UseIcgNetCoreUtilities(); -``` - -### Basic Usage - -```csharp -using ICG.NetCore.Utilities; - -// Use QueryableExtensions -var users = dbContext.Users - .WhereIf(activeOnly, u => u.IsActive) - .OrderByIf(sortByName, u => u.Name) - .GetPage(pageNumber, pageSize) - .ToList(); - -// Use EnumExtensions -var displayName = myEnum.GetDisplayNameOrStringValue(); - -// Use IdentityExtensions -var email = User.Identity.GetClaimValue(ClaimTypes.Email); -``` - -## Documentation Organization - -- **Main README**: High-level overview and quick reference for all features -- **Detailed Docs**: In-depth documentation for specific features with examples and best practices - - [QueryableExtensions.md](QueryableExtensions.md) - Complete guide to IQueryable extensions - - [EnumExtensions.md](EnumExtensions.md) - Complete guide to Enum extensions - - [EncryptionServices.md](EncryptionServices.md) - Complete guide to encryption services - -## Contributing - -See [CONTRIBUTING.md](../CONTRIBUTING.md) for information on how to contribute to this project. - -## Support - -For issues, questions, or contributions, please visit the [GitHub repository](https://github.com/IowaComputerGurus/netcore.utilities). - -## Additional Resources - -- [Source Code](https://github.com/IowaComputerGurus/netcore.utilities) -- [NuGet Package](https://www.nuget.org/packages/ICG.NetCore.Utilities/) -- [Release Notes](https://github.com/IowaComputerGurus/netcore.utilities/releases)