From 9b2bffa1dc47257fdbb9ea5226b1383a44e8c021 Mon Sep 17 00:00:00 2001 From: ChristianHaase Date: Sat, 29 Nov 2025 12:38:23 +0100 Subject: [PATCH 1/6] Bump dev version [skip ci] --- Directory.Version.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Version.props b/Directory.Version.props index 317e204..c38919a 100644 --- a/Directory.Version.props +++ b/Directory.Version.props @@ -1,6 +1,6 @@ - 10.0.0 + 10.0.1 \ No newline at end of file From aae5a842754adb9f2529b64526edde9529a4843f Mon Sep 17 00:00:00 2001 From: detilium Date: Sun, 30 Nov 2025 11:19:33 +0100 Subject: [PATCH 2/6] include package icon [skip ci] --- assets/icon.png | Bin 0 -> 7084 bytes ...lidator.Extensions.DependencyInjection.csproj | 2 ++ 2 files changed, 2 insertions(+) create mode 100644 assets/icon.png diff --git a/assets/icon.png b/assets/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..8cc0687662f7529bf0bfbc6156ca6fd814363689 GIT binary patch literal 7084 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 byteguard file-validator extensions dependency injection README.md + icon.png Copyright © ByteGuard Contributors MIT @@ -21,6 +22,7 @@ + From f2ba13071315284424965d2268c50aaa75674cdf Mon Sep 17 00:00:00 2001 From: detilium Date: Mon, 1 Dec 2025 11:32:58 +0100 Subject: [PATCH 3/6] feat: support antimalware scanners [skip ci] --- README.md | 49 ++++- ...ator.Extensions.DependencyInjection.csproj | 4 +- .../FileValidatorSettingsConfiguration.cs | 5 + .../Configuration/ScannerRegistration.cs | 84 ++++++++ .../ServiceCollectionExtensions.cs | 116 ++++++++++ .../MockAntimalwareScanner.cs | 29 +++ .../ServiceCollectionExtensionsTests.cs | 204 ++++++++++++++++++ 7 files changed, 483 insertions(+), 8 deletions(-) create mode 100644 src/ByteGuard.FileValidator.Extensions.DependencyInjection/Configuration/ScannerRegistration.cs create mode 100644 tests/FileValidator.Extensions.DependencyInjection.Tests.Unit/MockAntimalwareScanner.cs diff --git a/README.md b/README.md index 8bee196..61359d5 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ It gives you: - Extension methods to register the file validator in the DI container -- Easy configuration via appsettings.json or fluent configuration in code +- 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). @@ -12,7 +12,7 @@ It gives you: ## Getting Started ### Installation -This package is published and installed via NuGet. +This package is published and installed via [NuGet](https://www.nuget.org/packages/ByteGuard.FileValidator.Extensions.DependencyInjection). Reference the package in your project: ```bash @@ -24,20 +24,32 @@ dotnet add package ByteGuard.FileValidator.Extensions.DependencyInjection ### Add to DI container In your `Program.cs` (or `Startup.cs` in older projects), register the validator: +**Using inline configuration** ```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); + + // If an antimalware package has been added + options.Scanner = ScannerRegistration.Create(opts => + { + // Refer to the individual scanner implementations for ScannerType value and possible options. + // ... + }) }); +``` -// Using configuration from appsettings.json -builder.Services.AddFileValidator(options => configuration.GetSection("FileValidatorConfiguration").Bind(options)); +**Using configuration from appsettings.json with default "FileValidator" section name** +```csharp +builder.Services.AddFileValidator(builder.Configuration); +``` + +**Using configuration from appsettings.json with custom section name** +```csharp +builder.Services.AddFileValidator(builder.Configuration, "MySection"); ``` ### Injection & Usage @@ -79,5 +91,28 @@ It's possible to configure the `FileValidator` through `appsettings.json`. } ``` +**With antimalware scanner** +It's possible to configure an antimalware scanner directly through `appsettings.json`. + +> ℹ️ _Refer to the individual scanner implementations for `ScannerType` value and possible options._ + +```json +{ + "FileValidatorConfiguration": { + "SupportedFileTypes": [ ".pdf", ".jpg", ".png" ], + "FileSizeLimit": 26214400, + "UnitFileSizeLimit": "25MB", + "ThrowExceptionOnInvalidFile": true, + "Scanner": { + "ScannerType": "...", + "Options": { + "OptionA": "...", + "OptionB": "..." + } + } + } +} +``` + ## 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 975d804..612bf86 100644 --- a/src/ByteGuard.FileValidator.Extensions.DependencyInjection/ByteGuard.FileValidator.Extensions.DependencyInjection.csproj +++ b/src/ByteGuard.FileValidator.Extensions.DependencyInjection/ByteGuard.FileValidator.Extensions.DependencyInjection.csproj @@ -15,9 +15,11 @@ - + + + diff --git a/src/ByteGuard.FileValidator.Extensions.DependencyInjection/Configuration/FileValidatorSettingsConfiguration.cs b/src/ByteGuard.FileValidator.Extensions.DependencyInjection/Configuration/FileValidatorSettingsConfiguration.cs index d368d9d..f16d7d7 100644 --- a/src/ByteGuard.FileValidator.Extensions.DependencyInjection/Configuration/FileValidatorSettingsConfiguration.cs +++ b/src/ByteGuard.FileValidator.Extensions.DependencyInjection/Configuration/FileValidatorSettingsConfiguration.cs @@ -44,4 +44,9 @@ public class FileValidatorSettingsConfiguration /// Whether to throw an exception if an unsupported/invalid file is encountered. Defaults to true. /// public bool ThrowExceptionOnInvalidFile { get; set; } = true; + + /// + /// Configuration for the antimalware scanner to use. + /// + public ScannerRegistration? Scanner { get; set; } } diff --git a/src/ByteGuard.FileValidator.Extensions.DependencyInjection/Configuration/ScannerRegistration.cs b/src/ByteGuard.FileValidator.Extensions.DependencyInjection/Configuration/ScannerRegistration.cs new file mode 100644 index 0000000..9585361 --- /dev/null +++ b/src/ByteGuard.FileValidator.Extensions.DependencyInjection/Configuration/ScannerRegistration.cs @@ -0,0 +1,84 @@ +using ByteGuard.FileValidator.Scanners; +using Microsoft.Extensions.Configuration; + +namespace ByteGuard.FileValidator.Extensions.DependencyInjection.Configuration; + +/// +/// Registration information for an antimalware scanner. +/// +public sealed class ScannerRegistration +{ + /// + /// Type of the scanner to register. Must be derived from IAntimalwareScanner. + /// + public string ScannerType { get; set; } = default!; + + private Type? _scannerType; + + /// + /// Gets or sets the of the scanner. + /// + public Type Type + { + get + { + if (_scannerType is not null) + return _scannerType; + + _scannerType = Type.GetType(ScannerType, throwOnError: false)!; + return _scannerType; + } + set + { + _scannerType = value; + ScannerType = value.AssemblyQualifiedName!; + } + } + + /// + /// Options for the scanner. + /// + public object? OptionsInstance { get; set; } + + /// + /// Raw configuration section for options (for appsettings registration). + /// + public IConfigurationSection? OptionsConfiguration { get; set; } + + /// + /// Creates a new instance for the specified scanner type and options. + /// + /// Scanner options. + /// Scanner implementation inheriting from IAntimalwareScanner. + /// Scanner options. + public static ScannerRegistration Create(Action options) + where TScanner : IAntimalwareScanner + where TOptions : class, new() + { + var opts = new TOptions(); + options?.Invoke(opts); + + return new ScannerRegistration + { + Type = typeof(TScanner), + OptionsInstance = opts + }; + } + + /// + /// Creates a new instance for the specified scanner type and options. + /// + /// Scanner options. + /// Scanner implementation inheriting from IAntimalwareScanner. + /// Scanner options. + public static ScannerRegistration Create(TOptions options) + where TScanner : IAntimalwareScanner + where TOptions : class, new() + { + return new ScannerRegistration + { + Type = typeof(TScanner), + OptionsInstance = options + }; + } +} diff --git a/src/ByteGuard.FileValidator.Extensions.DependencyInjection/ServiceCollectionExtensions.cs b/src/ByteGuard.FileValidator.Extensions.DependencyInjection/ServiceCollectionExtensions.cs index b755b12..e7d4c90 100644 --- a/src/ByteGuard.FileValidator.Extensions.DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/ByteGuard.FileValidator.Extensions.DependencyInjection/ServiceCollectionExtensions.cs @@ -1,5 +1,7 @@ using ByteGuard.FileValidator.Configuration; using ByteGuard.FileValidator.Extensions.DependencyInjection.Configuration; +using ByteGuard.FileValidator.Scanners; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -10,6 +12,35 @@ namespace ByteGuard.FileValidator.Extensions.DependencyInjection; /// public static class ServiceCollectionExtensions { + /// + /// The default configuration section name for File Validator settings. + /// + public const string DefaultSectionName = "FileValidator"; + + /// + /// Adds the File Validator services to the specified with configuration from the provided . + /// + /// + /// This method binds the configuration section named "FileValidator" by default. + /// + /// Service collection. + /// Configuration. + public static IServiceCollection AddFileValidator(this IServiceCollection services, IConfiguration configuration) + { + return services.AddFileValidator(options => configuration.GetSection(DefaultSectionName).Bind(options)); + } + + /// + /// Adds the File Validator services to the specified with configuration from the provided . + /// + /// Service collection. + /// Configuration. + /// Section name. + public static IServiceCollection AddFileValidator(this IServiceCollection services, IConfiguration configuration, string sectionName) + { + return services.AddFileValidator(options => configuration.GetSection(sectionName).Bind(options)); + } + /// /// Adds the File Validator services to the specified with custom configuration options. /// @@ -17,6 +48,9 @@ public static class ServiceCollectionExtensions /// Configuration options. public static IServiceCollection AddFileValidator(this IServiceCollection services, Action options) { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(options); + // Validate and setup configuration options. services.AddSingleton, FileValidatorConfigurationOptionsValidator>(); @@ -41,13 +75,95 @@ public static IServiceCollection AddFileValidator(this IServiceCollection servic }) .ValidateOnStart(); + // Register antimalware scanner. + var settings = new FileValidatorSettingsConfiguration(); + options(settings); + + RegisterConfiguredScanner(services, settings.Scanner); + // Register the FileValidator service. services.AddSingleton(serviceProvider => { var configuration = serviceProvider.GetRequiredService>().Value; + + // If an antimalware scanner is registered, resolve it and pass it to the FileValidator. + var antimalwareScanner = serviceProvider.GetService(); + if (antimalwareScanner is not null) + { + return new FileValidator(configuration, antimalwareScanner); + } + + // No antimalware scanner registered. return new FileValidator(configuration); }); return services; } + + /// + /// Registers the configured antimalware scanner. + /// + /// Service collection. + /// Scanner registration. + private static void RegisterConfiguredScanner(IServiceCollection services, ScannerRegistration? scanner) + { + // If no scanner has been registerered. + if (scanner is null) return; + + var scannerType = scanner.Type; + if (scannerType is null) + { + throw new InvalidOperationException("The specified scanner type could not be resolved."); + } + + if (!typeof(IAntimalwareScanner).IsAssignableFrom(scanner.Type)) + { + throw new InvalidOperationException($"The specified scanner type '{scanner.Type.FullName}' does not implement the '{nameof(IAntimalwareScanner)}' interface."); + } + + var genericScannerInterface = scanner.Type + .GetInterfaces() + .FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IAntimalwareScanner<>)); + + var optionsType = genericScannerInterface?.GetGenericArguments()[0]; + + // Register scanner as IAntimalwareScanner implementation. + services.AddSingleton(_ => + { + object? optionsInstance = scanner.OptionsInstance; + if (optionsInstance is null && scanner.OptionsConfiguration is not null) + { + if (optionsType is null) + { + throw new InvalidOperationException($"Scanner '{scannerType.FullName}' must implement IAntimalwareScanner to be configured from appsettings."); + } + + optionsInstance = Activator.CreateInstance(optionsType); + if (optionsInstance is null) + { + throw new InvalidOperationException($"Could not create options instance of type '{optionsType.FullName}'."); + } + + scanner.OptionsConfiguration.Bind(optionsInstance); + } + + object? impl; + + if (optionsInstance is not null) + { + impl = Activator.CreateInstance(scanner.Type, optionsInstance); + } + else + { + impl = Activator.CreateInstance(scanner.Type); + } + + if (impl is not IAntimalwareScanner typedScanner) + { + throw new InvalidOperationException($"Scanner type '{scanner.Type.FullName}' does not implement the '{nameof(IAntimalwareScanner)}' interface."); + } + + return typedScanner; + }); + } } diff --git a/tests/FileValidator.Extensions.DependencyInjection.Tests.Unit/MockAntimalwareScanner.cs b/tests/FileValidator.Extensions.DependencyInjection.Tests.Unit/MockAntimalwareScanner.cs new file mode 100644 index 0000000..dba1301 --- /dev/null +++ b/tests/FileValidator.Extensions.DependencyInjection.Tests.Unit/MockAntimalwareScanner.cs @@ -0,0 +1,29 @@ +using ByteGuard.FileValidator.Scanners; + +namespace FileValidator.Extensions.DependencyInjection.Tests.Unit; + +public class MockAntimalwareScanner : IAntimalwareScanner +{ + public readonly MockAntimalwareScannerOptions Options; + + public MockAntimalwareScanner(MockAntimalwareScannerOptions options) + { + Options = options; + } + + public MockAntimalwareScanner() + { + Options = new MockAntimalwareScannerOptions(); + } + + public bool IsClean(Stream contentStream, string fileName) + { + return true; + } +} + +public class MockAntimalwareScannerOptions +{ + public string OptionA { get; set; } = string.Empty; + public int? OptionB { get; set; } +} diff --git a/tests/FileValidator.Extensions.DependencyInjection.Tests.Unit/ServiceCollectionExtensionsTests.cs b/tests/FileValidator.Extensions.DependencyInjection.Tests.Unit/ServiceCollectionExtensionsTests.cs index 092c16a..7544ca6 100644 --- a/tests/FileValidator.Extensions.DependencyInjection.Tests.Unit/ServiceCollectionExtensionsTests.cs +++ b/tests/FileValidator.Extensions.DependencyInjection.Tests.Unit/ServiceCollectionExtensionsTests.cs @@ -1,5 +1,8 @@ using ByteGuard.FileValidator.Configuration; using ByteGuard.FileValidator.Extensions.DependencyInjection.Configuration; +using ByteGuard.FileValidator.Scanners; +using FileValidator.Extensions.DependencyInjection.Tests.Unit; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -85,4 +88,205 @@ public void AddFileValidator_FileSizeLimitAndFriendlyFileSizeLimitBothSet_Should var options = sp.GetRequiredService>(); Assert.Equal(ByteSize.MegaBytes(10), options.Value.FileSizeLimit); } + + [Fact(DisplayName = "AddFileValidator should register the configured antimalware scanner")] + public void AddFileValidator_ConfiguredAntimalwareScanner_ShouldRegisterScanner() + { + // Arrange + var services = new ServiceCollection(); + Action configAction = options => + { + options.SupportedFileTypes = [".pdf"]; + options.FileSizeLimit = ByteSize.MegaBytes(15); + options.Scanner = ScannerRegistration.Create(opts => + { + opts.OptionA = "TestOption"; + opts.OptionB = 42; + }); + }; + + // Act + services.AddFileValidator(configAction); + + // Assert + var sp = services.BuildServiceProvider(); + var scanner = sp.GetRequiredService(); + Assert.IsType(scanner); + } + + [Fact(DisplayName = "AddFileValidator should register the configured antimalware scanner without options")] + public void AddFileValidator_ConfiguredAntimalwareScannerWithoutOptions_ShouldRegisterScanner() + { + // Arrange + var services = new ServiceCollection(); + Action configAction = options => + { + options.SupportedFileTypes = [".pdf"]; + options.FileSizeLimit = ByteSize.MegaBytes(15); + options.Scanner = ScannerRegistration.Create(_ => { }); + }; + + // Act + services.AddFileValidator(configAction); + + // Assert + var sp = services.BuildServiceProvider(); + var scanner = sp.GetRequiredService(); + Assert.IsType(scanner); + } + + [Fact(DisplayName = "AddFileValidator should not register an antimalware scanner when none is configured")] + public void AddFileValidator_NoConfiguredAntimalwareScanner_ShouldNotRegisterScanner() + { + // Arrange + var services = new ServiceCollection(); + Action configAction = options => + { + options.SupportedFileTypes = [".pdf"]; + options.FileSizeLimit = ByteSize.MegaBytes(15); + options.Scanner = null; // No scanner configured + }; + + // Act + services.AddFileValidator(configAction); + + // Assert + var sp = services.BuildServiceProvider(); + var scanner = sp.GetService(); + Assert.Null(scanner); + } + + [Fact(DisplayName = "AddFileValidator should throw exception when configured antimalware scanner type is invalid")] + public void AddFileValidator_ConfiguredAntimalwareScannerWithInvalidType_ShouldThrowException() + { + // Arrange + var services = new ServiceCollection(); + Action configAction = options => + { + options.SupportedFileTypes = [".pdf"]; + options.FileSizeLimit = ByteSize.MegaBytes(15); + options.Scanner = new ScannerRegistration + { + ScannerType = "Unknown.Namespace.ImplementationScanner, ImplementationScanner", // Invalid type, does not implement IAntimalwareScanner + OptionsInstance = null + }; + }; + + // Act & Assert + Assert.Throws(() => services.AddFileValidator(configAction)); + } + + [Fact(DisplayName = "AddFileValidator should inject the antimalware scanner into the FileValidator")] + public void AddFileValidator_ConfiguredAntimalwareScanner_ShouldInjectScannerIntoFileValidator() + { + // Arrange + var services = new ServiceCollection(); + Action configAction = options => + { + options.SupportedFileTypes = [".pdf"]; + options.FileSizeLimit = ByteSize.MegaBytes(15); + options.Scanner = ScannerRegistration.Create(opts => + { + opts.OptionA = "InjectedOption"; + opts.OptionB = 99; + }); + }; + + // Act + services.AddFileValidator(configAction); + + // Assert + var sp = services.BuildServiceProvider(); + var fileValidator = sp.GetRequiredService(); + Assert.NotNull(fileValidator); + + var scannerField = typeof(FileValidator).GetField("_antimalwareScanner", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + Assert.NotNull(scannerField); + + var scannerInstance = scannerField.GetValue(fileValidator) as MockAntimalwareScanner; + Assert.NotNull(scannerInstance); + } + + [Fact(DisplayName = "AddFileValidator should create FileValidator without antimalware scanner when none is configured")] + public void AddFileValidator_NoConfiguredAntimalwareScanner_ShouldCreateFileValidatorWithoutScanner() + { + // Arrange + var services = new ServiceCollection(); + Action configAction = options => + { + options.SupportedFileTypes = [".pdf"]; + options.FileSizeLimit = ByteSize.MegaBytes(15); + options.Scanner = null; // No scanner configured + }; + + // Act + services.AddFileValidator(configAction); + + // Assert + var sp = services.BuildServiceProvider(); + var fileValidator = sp.GetRequiredService(); + Assert.NotNull(fileValidator); + + var scannerField = typeof(FileValidator).GetField("_antimalwareScanner", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + Assert.NotNull(scannerField); + + var scannerInstance = scannerField.GetValue(fileValidator); + Assert.Null(scannerInstance); + } + + [Fact(DisplayName = "AddFileValidator should register the scanner when configuration has been provided through appsettings.json")] + public void AddFileValidator_ConfigurationFromAppSettings_ShouldRegisterScanner() + { + // Arrange + var services = new ServiceCollection(); + + var inMemorySettings = new Dictionary + { + {"FileValidator:SupportedFileTypes:0", ".pdf"}, + {"FileValidator:UnitFileSizeLimit", "15MB"}, + {"FileValidator:Scanner:ScannerType", "FileValidator.Extensions.DependencyInjection.Tests.Unit.MockAntimalwareScanner, FileValidator.Extensions.DependencyInjection.Tests.Unit"}, + {"FileValidator:Scanner:Options:OptionA", "ConfigOption"}, + {"FileValidator:Scanner:Options:OptionB", "123"} + }; + + IConfiguration configuration = new ConfigurationBuilder() + .AddInMemoryCollection(inMemorySettings.AsEnumerable()) + .Build(); + + // Act + services.AddFileValidator(configuration); + + // Assert + var sp = services.BuildServiceProvider(); + var scanner = sp.GetRequiredService(); + Assert.IsType(scanner); + } + + [Fact(DisplayName = "AddFileValidator should register the scanner when configuration has been provided through appsettings.json")] + public void AddFileValidator_ConfigurationFromAppSettingsCustomSectionName_ShouldRegisterScanner() + { + // Arrange + var services = new ServiceCollection(); + + var inMemorySettings = new Dictionary + { + {"CustomName:SupportedFileTypes:0", ".pdf"}, + {"CustomName:UnitFileSizeLimit", "15MB"}, + {"CustomName:Scanner:ScannerType", "FileValidator.Extensions.DependencyInjection.Tests.Unit.MockAntimalwareScanner, FileValidator.Extensions.DependencyInjection.Tests.Unit"}, + {"CustomName:Scanner:Options:OptionA", "ConfigOption"}, + {"CustomName:Scanner:Options:OptionB", "123"} + }; + + IConfiguration configuration = new ConfigurationBuilder() + .AddInMemoryCollection(inMemorySettings.AsEnumerable()) + .Build(); + + // Act + services.AddFileValidator(configuration, "CustomName"); + + // Assert + var sp = services.BuildServiceProvider(); + var scanner = sp.GetRequiredService(); + Assert.IsType(scanner); + } } From 27ccb4fab0f179c59a74b2eaae4bc065f6276675 Mon Sep 17 00:00:00 2001 From: detilium Date: Mon, 1 Dec 2025 12:03:52 +0100 Subject: [PATCH 4/6] refactor: clean up di registration [skip ci] --- .../ServiceCollectionExtensions.cs | 73 +++++++++++++------ 1 file changed, 52 insertions(+), 21 deletions(-) diff --git a/src/ByteGuard.FileValidator.Extensions.DependencyInjection/ServiceCollectionExtensions.cs b/src/ByteGuard.FileValidator.Extensions.DependencyInjection/ServiceCollectionExtensions.cs index e7d4c90..0fd9abb 100644 --- a/src/ByteGuard.FileValidator.Extensions.DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/ByteGuard.FileValidator.Extensions.DependencyInjection/ServiceCollectionExtensions.cs @@ -27,7 +27,7 @@ public static class ServiceCollectionExtensions /// Configuration. public static IServiceCollection AddFileValidator(this IServiceCollection services, IConfiguration configuration) { - return services.AddFileValidator(options => configuration.GetSection(DefaultSectionName).Bind(options)); + return services.AddFileValidator(configuration, DefaultSectionName); } /// @@ -38,47 +38,80 @@ public static IServiceCollection AddFileValidator(this IServiceCollection servic /// Section name. public static IServiceCollection AddFileValidator(this IServiceCollection services, IConfiguration configuration, string sectionName) { - return services.AddFileValidator(options => configuration.GetSection(sectionName).Bind(options)); + ArgumentNullException.ThrowIfNull(configuration); + ArgumentException.ThrowIfNullOrEmpty(sectionName); + + var section = configuration.GetSection(sectionName); + if (section is null) + { + throw new InvalidOperationException($"Configuration section '{sectionName}' not found."); + } + + var settings = new FileValidatorSettingsConfiguration(); + section.Bind(settings); + + var scannerSection = section.GetSection("Scanner"); + if (scannerSection.Exists()) + { + var scannerType = scannerSection["ScannerType"]; + if (string.IsNullOrWhiteSpace(scannerType)) + { + throw new InvalidOperationException("ScannerType must be specified in the configuration section."); + } + + settings.Scanner = new ScannerRegistration(); + settings.Scanner.ScannerType = scannerType; + settings.Scanner.OptionsConfiguration = scannerSection.GetSection("Options"); + } + + ConfigureFromSettings(services, settings); + return services; } /// /// Adds the File Validator services to the specified with custom configuration options. /// /// Service collection. - /// Configuration options. + /// File validator configuration. public static IServiceCollection AddFileValidator(this IServiceCollection services, Action options) { - ArgumentNullException.ThrowIfNull(services); ArgumentNullException.ThrowIfNull(options); - // Validate and setup configuration options. - services.AddSingleton, - FileValidatorConfigurationOptionsValidator>(); + var settings = new FileValidatorSettingsConfiguration(); + options(settings); - services.Configure(options); + ConfigureFromSettings(services, settings); + return services; + } + + /// + /// Configures services from settings. + /// + /// Service collection. + /// File valiator settings. + private static void ConfigureFromSettings(IServiceCollection services, FileValidatorSettingsConfiguration settings) + { + ArgumentNullException.ThrowIfNull(settings); services.AddOptions() - .Configure>((cfg, settings) => + .Configure(config => { // Convert from FileValidatorSettingsConfiguration to FileValidatorConfiguration. - cfg.SupportedFileTypes = settings.Value.SupportedFileTypes; - cfg.ThrowExceptionOnInvalidFile = settings.Value.ThrowExceptionOnInvalidFile; + config.SupportedFileTypes = settings.SupportedFileTypes; + config.ThrowExceptionOnInvalidFile = settings.ThrowExceptionOnInvalidFile; - if (settings.Value.FileSizeLimit != -1) + if (settings.FileSizeLimit != -1) { - cfg.FileSizeLimit = settings.Value.FileSizeLimit; + config.FileSizeLimit = settings.FileSizeLimit; } - else if (!string.IsNullOrWhiteSpace(settings.Value.UnitFileSizeLimit)) + else if (!string.IsNullOrWhiteSpace(settings.UnitFileSizeLimit)) { - cfg.FileSizeLimit = ByteSize.Parse(settings.Value.UnitFileSizeLimit); + config.FileSizeLimit = ByteSize.Parse(settings.UnitFileSizeLimit); } }) .ValidateOnStart(); - // Register antimalware scanner. - var settings = new FileValidatorSettingsConfiguration(); - options(settings); - + // Register antimalware scanner (if any). RegisterConfiguredScanner(services, settings.Scanner); // Register the FileValidator service. @@ -96,8 +129,6 @@ public static IServiceCollection AddFileValidator(this IServiceCollection servic // No antimalware scanner registered. return new FileValidator(configuration); }); - - return services; } /// From 6186a643a20bfdb62efc2ace1049cedaef57a3ec Mon Sep 17 00:00:00 2001 From: detilium Date: Mon, 1 Dec 2025 12:05:38 +0100 Subject: [PATCH 5/6] docs: update and clean up readme [skip ci] --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 61359d5..29a3008 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,6 @@ In your `Program.cs` (or `Startup.cs` in older projects), register the validator **Using inline configuration** ```csharp -// Using inline configuration builder.Services.AddFileValidator(options => { options.AllowFileTypes(FileExtensions.Pdf, FileExtensions.Jpg, FileExtensions.Png); From acfc67053bb1c4f71c9f655e4d93cf14ba810e7a Mon Sep 17 00:00:00 2001 From: detilium Date: Mon, 1 Dec 2025 13:10:16 +0100 Subject: [PATCH 6/6] prepare release v10.1.0 [skip ci] --- Directory.Version.props | 2 +- ...yteGuard.FileValidator.Extensions.DependencyInjection.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Version.props b/Directory.Version.props index c38919a..191820c 100644 --- a/Directory.Version.props +++ b/Directory.Version.props @@ -1,6 +1,6 @@ - 10.0.1 + 10.1.0 \ 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 612bf86..7fa6b5c 100644 --- a/src/ByteGuard.FileValidator.Extensions.DependencyInjection/ByteGuard.FileValidator.Extensions.DependencyInjection.csproj +++ b/src/ByteGuard.FileValidator.Extensions.DependencyInjection/ByteGuard.FileValidator.Extensions.DependencyInjection.csproj @@ -15,7 +15,7 @@ - +