diff --git a/src/Altinn.App.Api/Controllers/DataController.cs b/src/Altinn.App.Api/Controllers/DataController.cs index 1f168beb5..b7ed4ecf2 100644 --- a/src/Altinn.App.Api/Controllers/DataController.cs +++ b/src/Altinn.App.Api/Controllers/DataController.cs @@ -7,8 +7,6 @@ using Altinn.App.Core.Constants; using Altinn.App.Core.Extensions; using Altinn.App.Core.Features; -using Altinn.App.Core.Features.FileAnalysis; -using Altinn.App.Core.Features.FileAnalyzis; using Altinn.App.Core.Features.Validation; using Altinn.App.Core.Helpers; using Altinn.App.Core.Helpers.Serialization; @@ -46,9 +44,8 @@ public class DataController : ControllerBase private readonly IAppResources _appResourcesService; private readonly IAppMetadata _appMetadata; private readonly IPrefill _prefillService; - private readonly IFileAnalysisService _fileAnalyserService; - private readonly IFileValidationService _fileValidationService; private readonly IFeatureManager _featureManager; + private readonly IValidationService _validationService; private const long REQUEST_SIZE_LIMIT = 2000 * 1024 * 1024; /// @@ -64,8 +61,7 @@ public class DataController : ControllerBase /// The app metadata service /// The feature manager controlling enabled features. /// A service with prefill related logic. - /// Service used to analyse files uploaded. - /// Service used to validate files uploaded. + /// Service used to validate files uploaded. public DataController( ILogger logger, IInstanceClient instanceClient, @@ -75,10 +71,9 @@ public DataController( IAppModel appModel, IAppResources appResourcesService, IPrefill prefillService, - IFileAnalysisService fileAnalyserService, - IFileValidationService fileValidationService, IAppMetadata appMetadata, - IFeatureManager featureManager) + IFeatureManager featureManager, + IValidationService validationService) { _logger = logger; @@ -90,9 +85,8 @@ public DataController( _appResourcesService = appResourcesService; _appMetadata = appMetadata; _prefillService = prefillService; - _fileAnalyserService = fileAnalyserService; - _fileValidationService = fileValidationService; _featureManager = featureManager; + _validationService = validationService; } /// @@ -161,9 +155,8 @@ public async Task Create( StreamContent streamContent = Request.CreateContentStream(); - using Stream fileStream = new MemoryStream(); - await streamContent.CopyToAsync(fileStream); - if (fileStream.Length == 0) + var fileBytes = await streamContent.ReadAsByteArrayAsync(); + if (fileBytes.Length == 0) { const string errorMessage = "Invalid data provided. Error: The file is zero bytes."; var error = new ValidationIssue @@ -179,25 +172,14 @@ public async Task Create( bool parseSuccess = Request.Headers.TryGetValue("Content-Disposition", out StringValues headerValues); string? filename = parseSuccess ? DataRestrictionValidation.GetFileNameFromHeader(headerValues) : null; - IEnumerable fileAnalysisResults = new List(); - if (FileAnalysisEnabledForDataType(dataTypeFromMetadata)) - { - fileAnalysisResults = await _fileAnalyserService.Analyse(dataTypeFromMetadata, fileStream, filename); - } - - bool fileValidationSuccess = true; - List validationIssues = new(); - if (FileValidationEnabledForDataType(dataTypeFromMetadata)) - { - (fileValidationSuccess, validationIssues) = await _fileValidationService.Validate(dataTypeFromMetadata, fileAnalysisResults); - } + var fileAnalysisResults = await _validationService.ValidateFileUpload(instance, dataTypeFromMetadata, fileBytes, filename); - if (!fileValidationSuccess) + if (!fileAnalysisResults.All(r => r.Severity is ValidationIssueSeverity.Informational or ValidationIssueSeverity.Fixed)) { - return BadRequest(await GetErrorDetails(validationIssues)); + return BadRequest(await GetErrorDetails(fileAnalysisResults)); } - fileStream.Seek(0, SeekOrigin.Begin); + using var fileStream = new MemoryStream(fileBytes); return await CreateBinaryData(instance, dataType, streamContent.Headers.ContentType.ToString(), filename, fileStream); } } @@ -220,16 +202,6 @@ private async Task GetErrorDetails(List errors) return await _featureManager.IsEnabledAsync(FeatureFlags.JsonObjectInDataResponse) ? errors : string.Join(";", errors.Select(x => x.Description)); } - private static bool FileAnalysisEnabledForDataType(DataType dataTypeFromMetadata) - { - return dataTypeFromMetadata.EnabledFileAnalysers != null && dataTypeFromMetadata.EnabledFileAnalysers.Count > 0; - } - - private static bool FileValidationEnabledForDataType(DataType dataTypeFromMetadata) - { - return dataTypeFromMetadata.EnabledFileValidators != null && dataTypeFromMetadata.EnabledFileValidators.Count > 0; - } - /// /// Gets a data element from storage and applies business logic if nessesary. /// @@ -296,6 +268,8 @@ public async Task Get( /// unique id to identify the data element to update /// The updated data element, including the changed fields in the event of a calculation that changed data. [Authorize(Policy = AuthzConstants.POLICY_INSTANCE_WRITE)] + + // "{org}/{app}/instances/{instanceOwnerPartyId:int}/{instanceGuid:guid}/data/form/{dataGuid:guid}" [HttpPut("{dataGuid:guid}")] [DisableFormValueModelBinding] [RequestSizeLimit(REQUEST_SIZE_LIMIT)] diff --git a/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs b/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs index 378d01c54..a566d7921 100644 --- a/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs +++ b/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs @@ -4,7 +4,6 @@ using Altinn.App.Core.Features.Action; using Altinn.App.Core.Features.DataLists; using Altinn.App.Core.Features.DataProcessing; -using Altinn.App.Core.Features.FileAnalyzis; using Altinn.App.Core.Features.Options; using Altinn.App.Core.Features.PageOrder; using Altinn.App.Core.Features.Pdf; @@ -163,8 +162,6 @@ public static void AddAppServices(this IServiceCollection services, IConfigurati AddPdfServices(services); AddEventServices(services); AddProcessServices(services); - AddFileAnalyserServices(services); - AddFileValidatorServices(services); AddMetricsDecorators(services, configuration); if (!env.IsDevelopment()) @@ -255,18 +252,6 @@ private static void AddActionServices(IServiceCollection services) services.AddTransientUserActionAuthorizerForActionInAllTasks("sign"); } - private static void AddFileAnalyserServices(IServiceCollection services) - { - services.TryAddTransient(); - services.TryAddTransient(); - } - - private static void AddFileValidatorServices(IServiceCollection services) - { - services.TryAddTransient(); - services.TryAddTransient(); - } - private static void AddMetricsDecorators(IServiceCollection services, IConfiguration configuration) { MetricsSettings metricsSettings = configuration.GetSection("MetricsSettings")?.Get() ?? new MetricsSettings(); diff --git a/src/Altinn.App.Core/Features/FileAnalysis/FileAnalysisResult.cs b/src/Altinn.App.Core/Features/FileAnalysis/FileAnalysisResult.cs new file mode 100644 index 000000000..b5760b42d --- /dev/null +++ b/src/Altinn.App.Core/Features/FileAnalysis/FileAnalysisResult.cs @@ -0,0 +1,41 @@ +namespace Altinn.App.Core.Features.FileAnalysis; + +/// +/// Results from a file analysis done based the content of the file, ie. the binary data. +/// +public class FileAnalysisResult +{ + /// + /// Initializes a new instance of the class. + /// + public FileAnalysisResult(string analyserId) + { + AnalyserId = analyserId; + } + + /// + /// The id of the analyser generating the result. + /// + public string AnalyserId { get; internal set; } + + /// + /// The name of the analysed file. + /// + public string? Filename { get; set; } + + /// + /// The file extension(s) without the . i.e. pdf | png | docx + /// Some mime types might have multiple extensions registered for ecample image/jpeg has both jpg and jpeg. + /// + public List Extensions { get; set; } = new List(); + + /// + /// The mime type + /// + public string? MimeType { get; set; } + + /// + /// Key/Value pairs containing findings from the analysis. + /// + public IDictionary Metadata { get; private set; } = new Dictionary(); +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Features/FileAnalysis/IFileAnalyser.cs b/src/Altinn.App.Core/Features/FileAnalysis/IFileAnalyser.cs new file mode 100644 index 000000000..804e98929 --- /dev/null +++ b/src/Altinn.App.Core/Features/FileAnalysis/IFileAnalyser.cs @@ -0,0 +1,19 @@ +namespace Altinn.App.Core.Features.FileAnalysis; + +/// +/// Interface for doing extended binary file analysing. +/// +public interface IFileAnalyser +{ + /// + /// The id of the analyser to be used when enabling it from config. + /// + public string Id { get; } + + /// + /// Analyses a stream with the intent to extract metadata. + /// + /// The stream to analyse. One stream = one file. + /// Filename. Optional parameter if the implementation needs the name of the file, relative or absolute path. + public Task Analyse(Stream stream, string? filename = null); +} \ No newline at end of file diff --git a/src/Altinn.App.Core/Features/FileAnalyzis/FileAnalyserFactory.cs b/src/Altinn.App.Core/Features/FileAnalyzis/FileAnalyserFactory.cs deleted file mode 100644 index 6ed150674..000000000 --- a/src/Altinn.App.Core/Features/FileAnalyzis/FileAnalyserFactory.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Altinn.App.Core.Features.FileAnalysis; -using Altinn.Platform.Storage.Interface.Models; - -namespace Altinn.App.Core.Features.FileAnalyzis -{ - /// - /// Factory class that resolves the correct file analysers to run on against a . - /// - public class FileAnalyserFactory : IFileAnalyserFactory - { - private readonly IEnumerable _fileAnalysers; - - /// - /// Initializes a new instance of the class. - /// - public FileAnalyserFactory(IEnumerable fileAnalysers) - { - _fileAnalysers = fileAnalysers; - } - - /// - /// Finds the specified file analyser implementations based on the specified analyser id's. - /// - public IEnumerable GetFileAnalysers(IEnumerable analyserIds) - { - return _fileAnalysers.Where(x => analyserIds.Contains(x.Id)).ToList(); - } - } -} diff --git a/src/Altinn.App.Core/Features/FileAnalyzis/FileAnalysisResult.cs b/src/Altinn.App.Core/Features/FileAnalyzis/FileAnalysisResult.cs deleted file mode 100644 index 279cd97d3..000000000 --- a/src/Altinn.App.Core/Features/FileAnalyzis/FileAnalysisResult.cs +++ /dev/null @@ -1,44 +0,0 @@ -using Altinn.App.Core.Features.FileAnalyzis; - -namespace Altinn.App.Core.Features.FileAnalysis -{ - /// - /// Results from a file analysis done based the content of the file, ie. the binary data. - /// - public class FileAnalysisResult - { - /// - /// Initializes a new instance of the class. - /// - public FileAnalysisResult(string analyserId) - { - AnalyserId = analyserId; - } - - /// - /// The id of the analyser generating the result. - /// - public string AnalyserId { get; internal set; } - - /// - /// The name of the analysed file. - /// - public string? Filename { get; set; } - - /// - /// The file extension(s) without the . i.e. pdf | png | docx - /// Some mime types might have multiple extensions registered for ecample image/jpeg has both jpg and jpeg. - /// - public List Extensions { get; set; } = new List(); - - /// - /// The mime type - /// - public string? MimeType { get; set; } - - /// - /// Key/Value pairs containing findings from the analysis. - /// - public IDictionary Metadata { get; private set; } = new Dictionary(); - } -} diff --git a/src/Altinn.App.Core/Features/FileAnalyzis/FileAnalysisService.cs b/src/Altinn.App.Core/Features/FileAnalyzis/FileAnalysisService.cs deleted file mode 100644 index 84c13d0d8..000000000 --- a/src/Altinn.App.Core/Features/FileAnalyzis/FileAnalysisService.cs +++ /dev/null @@ -1,44 +0,0 @@ -using Altinn.App.Core.Features.FileAnalysis; -using Altinn.Platform.Storage.Interface.Models; - -namespace Altinn.App.Core.Features.FileAnalyzis -{ - /// - /// Analyses a file using the registred analysers on the - /// - public class FileAnalysisService : IFileAnalysisService - { - private readonly IFileAnalyserFactory _fileAnalyserFactory; - - /// - /// Initializes a new instance of the class. - /// - public FileAnalysisService(IFileAnalyserFactory fileAnalyserFactory) - { - _fileAnalyserFactory = fileAnalyserFactory; - } - - /// - /// Runs the specified file analysers against the stream provided. - /// - public async Task> Analyse(DataType dataType, Stream fileStream, string? filename = null) - { - List fileAnalysers = _fileAnalyserFactory.GetFileAnalysers(dataType.EnabledFileAnalysers).ToList(); - - List fileAnalysisResults = new(); - foreach (var analyser in fileAnalysers) - { - if (fileStream.CanSeek) - { - fileStream.Position = fileStream.Seek(0, SeekOrigin.Begin); - } - var result = await analyser.Analyse(fileStream, filename); - result.AnalyserId = analyser.Id; - result.Filename = filename; - fileAnalysisResults.Add(result); - } - - return fileAnalysisResults; - } - } -} diff --git a/src/Altinn.App.Core/Features/FileAnalyzis/IFileAnalyser.cs b/src/Altinn.App.Core/Features/FileAnalyzis/IFileAnalyser.cs deleted file mode 100644 index e430da273..000000000 --- a/src/Altinn.App.Core/Features/FileAnalyzis/IFileAnalyser.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace Altinn.App.Core.Features.FileAnalysis -{ - /// - /// Interface for doing extended binary file analysing. - /// - public interface IFileAnalyser - { - /// - /// The id of the analyser to be used when enabling it from config. - /// - public string Id { get; } - - /// - /// Analyses a stream with the intent to extract metadata. - /// - /// The stream to analyse. One stream = one file. - /// Filename. Optional parameter if the implementation needs the name of the file, relative or absolute path. - public Task Analyse(Stream stream, string? filename = null); - } -} diff --git a/src/Altinn.App.Core/Features/FileAnalyzis/IFileAnalyserFactory.cs b/src/Altinn.App.Core/Features/FileAnalyzis/IFileAnalyserFactory.cs deleted file mode 100644 index 31a5f37eb..000000000 --- a/src/Altinn.App.Core/Features/FileAnalyzis/IFileAnalyserFactory.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Altinn.App.Core.Features.FileAnalysis; -using Altinn.Platform.Storage.Interface.Models; - -namespace Altinn.App.Core.Features.FileAnalyzis -{ - /// - /// Interface responsible for resolving the correct file analysers to run on against a . - /// - public interface IFileAnalyserFactory - { - /// - /// Finds analyser implementations based on the specified id's provided. - /// - IEnumerable GetFileAnalysers(IEnumerable analyserIds); - } -} \ No newline at end of file diff --git a/src/Altinn.App.Core/Features/FileAnalyzis/IFileAnalysisService.cs b/src/Altinn.App.Core/Features/FileAnalyzis/IFileAnalysisService.cs deleted file mode 100644 index 8ee841372..000000000 --- a/src/Altinn.App.Core/Features/FileAnalyzis/IFileAnalysisService.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Altinn.App.Core.Features.FileAnalysis; -using Altinn.Platform.Storage.Interface.Models; - -namespace Altinn.App.Core.Features.FileAnalyzis -{ - /// - /// Interface for running all file analysers registered on a data type. - /// - public interface IFileAnalysisService - { - /// - /// Analyses the the specified file stream. - /// - /// The where the anlysers are registered. - /// The file stream to analyse - /// The name of the file - Task> Analyse(DataType dataType, Stream fileStream, string? filename = null); - } -} \ No newline at end of file diff --git a/src/Altinn.App.Core/Features/Validation/FileValidationService.cs b/src/Altinn.App.Core/Features/Validation/FileValidationService.cs deleted file mode 100644 index 7ee943dce..000000000 --- a/src/Altinn.App.Core/Features/Validation/FileValidationService.cs +++ /dev/null @@ -1,44 +0,0 @@ -using Altinn.App.Core.Features.FileAnalysis; -using Altinn.App.Core.Models.Validation; -using Altinn.Platform.Storage.Interface.Models; - -namespace Altinn.App.Core.Features.Validation -{ - /// - /// Validates files according to the registered IFileValidation interfaces - /// - public class FileValidationService : IFileValidationService - { - private readonly IFileValidatorFactory _fileValidatorFactory; - - /// - /// Initializes a new instance of the class. - /// - public FileValidationService(IFileValidatorFactory fileValidatorFactory) - { - _fileValidatorFactory = fileValidatorFactory; - } - - /// - /// Runs all registered validators on the specified - /// - public async Task<(bool Success, List Errors)> Validate(DataType dataType, IEnumerable fileAnalysisResults) - { - List allErrors = new(); - bool allSuccess = true; - - List fileValidators = _fileValidatorFactory.GetFileValidators(dataType.EnabledFileValidators).ToList(); - foreach (IFileValidator fileValidator in fileValidators) - { - (bool success, IEnumerable errors) = await fileValidator.Validate(dataType, fileAnalysisResults); - if (!success) - { - allSuccess = false; - allErrors.AddRange(errors); - } - } - - return (allSuccess, allErrors); - } - } -} diff --git a/src/Altinn.App.Core/Features/Validation/FileValidatorFactory.cs b/src/Altinn.App.Core/Features/Validation/FileValidatorFactory.cs deleted file mode 100644 index aa607127d..000000000 --- a/src/Altinn.App.Core/Features/Validation/FileValidatorFactory.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Altinn.Platform.Storage.Interface.Models; - -namespace Altinn.App.Core.Features.Validation -{ - /// - /// Factory class that resolves the correct file validators to run on against a . - /// - public class FileValidatorFactory : IFileValidatorFactory - { - private readonly IEnumerable _fileValidators; - - /// - /// Initializes a new instance of the class. - /// - public FileValidatorFactory(IEnumerable fileValidators) - { - _fileValidators = fileValidators; - } - - /// - /// Finds the specified file analyser implementations based on the specified analyser id's. - /// - public IEnumerable GetFileValidators(IEnumerable validatorIds) - { - return _fileValidators.Where(x => validatorIds.Contains(x.Id)).ToList(); - } - } -} diff --git a/src/Altinn.App.Core/Features/Validation/IFileValidationService.cs b/src/Altinn.App.Core/Features/Validation/IFileValidationService.cs deleted file mode 100644 index c90f0ee25..000000000 --- a/src/Altinn.App.Core/Features/Validation/IFileValidationService.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Altinn.App.Core.Features.FileAnalysis; -using Altinn.App.Core.Models.Validation; -using Altinn.Platform.Storage.Interface.Models; - -namespace Altinn.App.Core.Features.Validation -{ - /// - /// Interface for running all file validators registered on a data type. - /// - public interface IFileValidationService - { - /// - /// Validates the file based on the file analysis results. - /// - Task<(bool Success, List Errors)> Validate(DataType dataType, IEnumerable fileAnalysisResults); - } -} \ No newline at end of file diff --git a/src/Altinn.App.Core/Features/Validation/IFileValidatorFactory.cs b/src/Altinn.App.Core/Features/Validation/IFileValidatorFactory.cs deleted file mode 100644 index 2c1f18c8c..000000000 --- a/src/Altinn.App.Core/Features/Validation/IFileValidatorFactory.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Altinn.Platform.Storage.Interface.Models; - -namespace Altinn.App.Core.Features.Validation -{ - /// - /// Interface responsible for resolving the correct file validators to run on against a . - /// - public interface IFileValidatorFactory - { - /// - /// Finds validator implementations based on the specified id's provided. - /// - IEnumerable GetFileValidators(IEnumerable validatorIds); - } -} \ No newline at end of file diff --git a/src/Altinn.App.Core/Features/Validation/IValidationService.cs b/src/Altinn.App.Core/Features/Validation/IValidationService.cs index 1f9ac0739..da4db7ae3 100644 --- a/src/Altinn.App.Core/Features/Validation/IValidationService.cs +++ b/src/Altinn.App.Core/Features/Validation/IValidationService.cs @@ -50,4 +50,14 @@ public interface IValidationService /// The data deserialized to the strongly typed object that represents the form data /// List of json paths for the fields that have changed (used for incremental validation) Task>> ValidateFormData(Instance instance, DataElement dataElement, DataType dataType, object data, List? changedFields = null); + + /// + /// Validate file uploads. This method executes validations for and + /// + /// The instance the file will be uploaded to + /// The data type of the file to be uploaded + /// The actual bytes of the file + /// The file name sent with the file + /// + Task> ValidateFileUpload(Instance instance, DataType dataType, byte[] fileContent, string? fileName = null); } \ No newline at end of file diff --git a/src/Altinn.App.Core/Features/Validation/ValidationService.cs b/src/Altinn.App.Core/Features/Validation/ValidationService.cs index 4280cd386..9780bc66f 100644 --- a/src/Altinn.App.Core/Features/Validation/ValidationService.cs +++ b/src/Altinn.App.Core/Features/Validation/ValidationService.cs @@ -1,7 +1,7 @@ +using Altinn.App.Core.Features.FileAnalysis; using Altinn.App.Core.Internal.App; using Altinn.App.Core.Internal.AppModel; using Altinn.App.Core.Internal.Data; -using Altinn.App.Core.Internal.Process.Elements; using Altinn.App.Core.Models.Validation; using Altinn.Platform.Storage.Interface.Models; using Microsoft.Extensions.DependencyInjection; @@ -158,4 +158,83 @@ public async Task>> ValidateFormData(In return dataValidators.Zip(issuesLists).ToDictionary(kv => kv.First.Code ?? string.Empty, kv => kv.Second); } -} \ No newline at end of file + + /// + public async Task> ValidateFileUpload(Instance instance, DataType dataType, byte[] fileBytes, string? fileName = null) + { + var fileAnalysisResults = new List(); + if (FileAnalysisEnabledForDataType(dataType)) + { + using var fileStream = new MemoryStream(fileBytes); + fileAnalysisResults = await Analyse(dataType, fileStream, fileName); + } + + bool fileValidationSuccess = true; + List validationIssues = new(); + if (FileValidationEnabledForDataType(dataType)) + { + (fileValidationSuccess, validationIssues) = await Validate(dataType, fileAnalysisResults); + } + + return validationIssues; + } + + private static bool FileAnalysisEnabledForDataType(DataType dataTypeFromMetadata) + { + return dataTypeFromMetadata.EnabledFileAnalysers != null && dataTypeFromMetadata.EnabledFileAnalysers.Count > 0; + } + + private static bool FileValidationEnabledForDataType(DataType dataTypeFromMetadata) + { + return dataTypeFromMetadata.EnabledFileValidators != null && dataTypeFromMetadata.EnabledFileValidators.Count > 0; + } + + private async Task<(bool Success, List Errors)> Validate(DataType dataType, List fileAnalysisResults) + { + List allErrors = new(); + bool allSuccess = true; + + var fileValidators = _serviceProvider.GetServices() + .Where(fv => dataType.EnabledFileValidators.Contains(fv.Id)) + .Concat(_serviceProvider.GetKeyedServices(dataType.Id)) + .ToList(); + + foreach (IFileValidator fileValidator in fileValidators) + { + (bool success, IEnumerable errors) = await fileValidator.Validate(dataType, fileAnalysisResults); + if (!success) + { + allSuccess = false; + allErrors.AddRange(errors); + } + } + + return (allSuccess, allErrors); + } + + /// + /// Runs the specified file analysers against the stream provided. + /// + private async Task> Analyse(DataType dataType, Stream fileStream, string? filename = null) + { + var fileAnalysers = _serviceProvider.GetServices() + .Where(fa => dataType.EnabledFileAnalysers.Contains(fa.Id)) + .Concat(_serviceProvider.GetKeyedServices(dataType.Id)); + + List fileAnalysisResults = new(); + foreach (var analyser in fileAnalysers) + { + if (fileStream.CanSeek) + { + fileStream.Position = fileStream.Seek(0, SeekOrigin.Begin); + } + var result = await analyser.Analyse(fileStream, filename); + result.AnalyserId = analyser.Id; + result.Filename = filename; + fileAnalysisResults.Add(result); + } + + return fileAnalysisResults; + } + +}