diff --git a/ByteGuard.FileValidator.Extensions.DependencyInjection.slnx b/ByteGuard.FileValidator.Extensions.DependencyInjection.slnx index 52a773b..2bfe89c 100644 --- a/ByteGuard.FileValidator.Extensions.DependencyInjection.slnx +++ b/ByteGuard.FileValidator.Extensions.DependencyInjection.slnx @@ -1,3 +1,12 @@ + + + + + + + + + diff --git a/README.md b/README.md index 0c543d4..8bee196 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,83 @@ # ByteGuard.FileValidator.Extensions.DependencyInjection ![NuGet Version](https://img.shields.io/nuget/v/ByteGuard.FileValidator.Extensions.DependencyInjection) +`ByteGuard.FileValidator.Extensions.DependencyInjection` provides first-class integration of `ByteGuard.FileValidator` with `Microsoft.Extensions.DependencyInjection`. + +It gives you: +- Extension methods to register the file validator in the DI container +- Easy configuration via appsettings.json or fluent configuration in code + +> This package is the `Microsoft.Extensions.DependencyInjection` integration layer. +> The core validation logic lives in [`ByteGuard.FileValidator`](https://github.com/ByteGuard-HQ/byteguard-file-validator-net). + +## Getting Started + +### Installation +This package is published and installed via NuGet. + +Reference the package in your project: +```bash +dotnet add package ByteGuard.FileValidator.Extensions.DependencyInjection +``` + +## Usage + +### Add to DI container +In your `Program.cs` (or `Startup.cs` in older projects), register the validator: + +```csharp +using ByteGuard.FileValidator; +using ByteGuard.FileValidator.Extensions.DependencyInjection; + +// Using inline configuration +builder.Services.AddFileValidator(options => +{ + options.AllowFileTypes(FileExtensions.Pdf, FileExtensions.Jpg, FileExtensions.Png); + options.FileSizeLimit = ByteSize.MegaBytes(25); + options.ThrowOnInvalidFiles(false); +}); + +// Using configuration from appsettings.json +builder.Services.AddFileValidator(options => configuration.GetSection("FileValidatorConfiguration").Bind(options)); +``` + +### Injection & Usage +You can then inject `FileValidator` into your services and other classes. + +```csharp +public class MyService +{ + private readonly FileValidator _fileValidator; + + public MyService(FileValidator fileValidator) + { + _fileValidator = fileValidator; + } + + public bool SaveFile(Stream fileStream, string fileName) + { + var isValid = _fileValidator.IsValidFile(fileName, fileStream); + + // ... + } +} +``` + +### Configuration via appsettings +It's possible to configure the `FileValidator` through `appsettings.json`. + +> _ℹ️ As you'll notice below, you can either define the `FileSizeLimit` in raw byte size, or use the `UnitFileSizeLimit` to define +> the file size in a more human readable format. When both are defined, `FileSizeLimit` always wins over `UnitFileSizeLimit`._ + +```json +{ + "FileValidatorConfiguration": { + "SupportedFileTypes": [ ".pdf", ".jpg", ".png" ], + "FileSizeLimit": 26214400, + "UnitFileSizeLimit": "25MB", + "ThrowExceptionOnInvalidFile": true + } +} +``` + +## License +_ByteGuard.FileValidator.Extensions.DpendencyInjection is Copyright © ByteGuard Contributors - Provided under the MIT license._ \ No newline at end of file diff --git a/src/ByteGuard.FileValidator.Extensions.DependencyInjection/ByteGuard.FileValidator.Extensions.DependencyInjection.csproj b/src/ByteGuard.FileValidator.Extensions.DependencyInjection/ByteGuard.FileValidator.Extensions.DependencyInjection.csproj index 45d1a79..7ea2af2 100644 --- a/src/ByteGuard.FileValidator.Extensions.DependencyInjection/ByteGuard.FileValidator.Extensions.DependencyInjection.csproj +++ b/src/ByteGuard.FileValidator.Extensions.DependencyInjection/ByteGuard.FileValidator.Extensions.DependencyInjection.csproj @@ -2,8 +2,8 @@ net8.0;net9.0;net10.0 - ByteGuard Contributors, detilium - ByteGuard File Validator support for ASP.NET Core. + ByteGuard Contributors + ByteGuard File Validator support for Microsoft.Extensions.DependencyInjection. https://github.com/ByteGuard-HQ/byteguard-file-validator-extensions-dependency-injection https://github.com/ByteGuard-HQ/byteguard-file-validator-extensions-dependency-injection git diff --git a/src/ByteGuard.FileValidator.Extensions.DependencyInjection/Configuration/FileValidatorConfigurationOptionsValidator.cs b/src/ByteGuard.FileValidator.Extensions.DependencyInjection/Configuration/FileValidatorConfigurationOptionsValidator.cs new file mode 100644 index 0000000..cbac2e7 --- /dev/null +++ b/src/ByteGuard.FileValidator.Extensions.DependencyInjection/Configuration/FileValidatorConfigurationOptionsValidator.cs @@ -0,0 +1,24 @@ +using ByteGuard.FileValidator.Configuration; +using Microsoft.Extensions.Options; + +namespace ByteGuard.FileValidator.Extensions.DependencyInjection.Configuration; + +/// +/// File validator configuration options validator for use with the options pattern. +/// +public class FileValidatorConfigurationOptionsValidator : IValidateOptions +{ + /// + public ValidateOptionsResult Validate(string? name, FileValidatorConfiguration config) + { + try + { + ConfigurationValidator.ThrowIfInvalid(config); + return ValidateOptionsResult.Success; + } + catch (Exception ex) + { + return ValidateOptionsResult.Fail(ex.Message); + } + } +} diff --git a/src/ByteGuard.FileValidator.Extensions.DependencyInjection/Configuration/FileValidatorSettingsConfiguration.cs b/src/ByteGuard.FileValidator.Extensions.DependencyInjection/Configuration/FileValidatorSettingsConfiguration.cs index 01695e6..d368d9d 100644 --- a/src/ByteGuard.FileValidator.Extensions.DependencyInjection/Configuration/FileValidatorSettingsConfiguration.cs +++ b/src/ByteGuard.FileValidator.Extensions.DependencyInjection/Configuration/FileValidatorSettingsConfiguration.cs @@ -1,6 +1,47 @@ namespace ByteGuard.FileValidator.Extensions.DependencyInjection.Configuration; +/// +/// Appsettings specific configuration for the File Validator. +/// public class FileValidatorSettingsConfiguration { + /// + /// Supported file types. + /// + /// + /// Setup which file types are supported by the file validator. + /// File types should be specified with their leading dot, e.g. .jpg, .png, .pdf, etc. + /// Currently supported file types can be found in . + /// + public List SupportedFileTypes { get; set; } = new List(); + /// + /// Maximum file size limit in bytes. + /// + /// + /// Defines the file size limit of files. See for conversion help. + /// + public long FileSizeLimit { get; set; } = -1; + + /// + /// Maximum file size limit in unit string representation (e.g. "25MB", "2 GB", etc.). + /// + /// + /// Will be ignored if is defined. + /// Spacing ("25 MB" vs. "25MB") is irrelevant. + /// Supported string representation are: + ///
    + ///
  • B: Bytes
  • + ///
  • KB: Kilobytes
  • + ///
  • MB: Megabytes
  • + ///
  • GB: Gigabytes
  • + ///
+ ///
+ ///
+ public string? UnitFileSizeLimit { get; set; } + + /// + /// Whether to throw an exception if an unsupported/invalid file is encountered. Defaults to true. + /// + public bool ThrowExceptionOnInvalidFile { get; set; } = true; } diff --git a/src/ByteGuard.FileValidator.Extensions.DependencyInjection/ServiceCollectionExtensions.cs b/src/ByteGuard.FileValidator.Extensions.DependencyInjection/ServiceCollectionExtensions.cs index 817b2ba..b755b12 100644 --- a/src/ByteGuard.FileValidator.Extensions.DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/ByteGuard.FileValidator.Extensions.DependencyInjection/ServiceCollectionExtensions.cs @@ -1,12 +1,52 @@ +using ByteGuard.FileValidator.Configuration; +using ByteGuard.FileValidator.Extensions.DependencyInjection.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; namespace ByteGuard.FileValidator.Extensions.DependencyInjection; +/// +/// Extension methods for registering the File Validator services in an . +/// public static class ServiceCollectionExtensions { - public static IServiceCollection AddFileValidator(this IServiceCollection services, Action configureSettings) + /// + /// Adds the File Validator services to the specified with custom configuration options. + /// + /// Service collection. + /// Configuration options. + public static IServiceCollection AddFileValidator(this IServiceCollection services, Action options) { - // TODO. + // Validate and setup configuration options. + services.AddSingleton, + FileValidatorConfigurationOptionsValidator>(); + + services.Configure(options); + + services.AddOptions() + .Configure>((cfg, settings) => + { + // Convert from FileValidatorSettingsConfiguration to FileValidatorConfiguration. + cfg.SupportedFileTypes = settings.Value.SupportedFileTypes; + cfg.ThrowExceptionOnInvalidFile = settings.Value.ThrowExceptionOnInvalidFile; + + if (settings.Value.FileSizeLimit != -1) + { + cfg.FileSizeLimit = settings.Value.FileSizeLimit; + } + else if (!string.IsNullOrWhiteSpace(settings.Value.UnitFileSizeLimit)) + { + cfg.FileSizeLimit = ByteSize.Parse(settings.Value.UnitFileSizeLimit); + } + }) + .ValidateOnStart(); + + // Register the FileValidator service. + services.AddSingleton(serviceProvider => + { + var configuration = serviceProvider.GetRequiredService>().Value; + return new FileValidator(configuration); + }); return services; } diff --git a/tests/FileValidator.Extensions.DependencyInjection.Tests.Unit/FileValidator.Extensions.DependencyInjection.Tests.Unit.csproj b/tests/FileValidator.Extensions.DependencyInjection.Tests.Unit/FileValidator.Extensions.DependencyInjection.Tests.Unit.csproj new file mode 100644 index 0000000..f28e02b --- /dev/null +++ b/tests/FileValidator.Extensions.DependencyInjection.Tests.Unit/FileValidator.Extensions.DependencyInjection.Tests.Unit.csproj @@ -0,0 +1,26 @@ + + + + net10.0 + false + true + + + + + + + + + + + + + + + + + + + + diff --git a/tests/FileValidator.Extensions.DependencyInjection.Tests.Unit/ServiceCollectionExtensionsTests.cs b/tests/FileValidator.Extensions.DependencyInjection.Tests.Unit/ServiceCollectionExtensionsTests.cs new file mode 100644 index 0000000..092c16a --- /dev/null +++ b/tests/FileValidator.Extensions.DependencyInjection.Tests.Unit/ServiceCollectionExtensionsTests.cs @@ -0,0 +1,88 @@ +using ByteGuard.FileValidator.Configuration; +using ByteGuard.FileValidator.Extensions.DependencyInjection.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace ByteGuard.FileValidator.Extensions.DependencyInjection.Tests.Unit; + +public class ServiceCollectionExtensionsTests +{ + [Fact(DisplayName = "AddFileValidator adds FileValidator to the service collection")] + public void AddFileValidator_AddsFileValidatorToServiceCollection() + { + // Arrange + var services = new ServiceCollection(); + + // Act + services.AddFileValidator(config => + { + config.SupportedFileTypes = new List() { ".png", ".jpg" }; + config.UnitFileSizeLimit = "25MB"; + }); + + var serviceProvider = services.BuildServiceProvider(); + + // Assert + var fileValidator = serviceProvider.GetService(); + Assert.NotNull(fileValidator); + } + + [Fact(DisplayName = "AddFileValidator should throws exception when configuration is invalid")] + public void AddFileValidator_ShouldThrowException_WhenConfigurationIsInvalid() + { + // Arrange + var services = new ServiceCollection(); + + // Act + services.AddFileValidator(config => + { + config.SupportedFileTypes = []; // Invalid configuration - missing supported file types + }); + + var serviceProvider = services.BuildServiceProvider(); + + // Assert + Assert.ThrowsAny(() => serviceProvider.GetRequiredService()); + } + + [Fact(DisplayName = "AddFileValidator should set correct file size limit when using the friendly file size limit property")] + public void AddFileValidator_FriendlyFileSizeLimitSet_ShouldAddAFileValidator() + { + // Arrange + var services = new ServiceCollection(); + Action configAction = options => + { + options.SupportedFileTypes = [".pdf"]; + options.UnitFileSizeLimit = "25MB"; + }; + + // Act + services.AddFileValidator(configAction); + + // Assert + var sp = services.BuildServiceProvider(); + var options = sp.GetRequiredService>(); + Assert.Equal(ByteSize.MegaBytes(25), options.Value.FileSizeLimit); + } + + [Fact(DisplayName = "AddFileValidator should use file size limit over friendly file size limit if both are set")] + public void AddFileValidator_FileSizeLimitAndFriendlyFileSizeLimitBothSet_ShouldUseFileSizeLimitOverFriendlyFileSizeLimit() + { + // Arrange + var services = new ServiceCollection(); + Action configAction = options => + { + options.SupportedFileTypes = [".pdf"]; + options.FileSizeLimit = ByteSize.MegaBytes(10); + options.UnitFileSizeLimit = "25MB"; + }; + + // Act + services.AddFileValidator(configAction); + + // Assert + var sp = services.BuildServiceProvider(); + var options = sp.GetRequiredService>(); + Assert.Equal(ByteSize.MegaBytes(10), options.Value.FileSizeLimit); + } +}