This commit is contained in:
Zeeshaun
2026-03-13 09:11:35 -05:00
parent f004c8205a
commit c2d6bda458
197 changed files with 14970 additions and 0 deletions
+364
View File
@@ -0,0 +1,364 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
/.idea/
/.vscode/
+37
View File
@@ -0,0 +1,37 @@
# .NET Core SDK
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS aspnetcore-build
# Copy Projects
COPY LCU.API.Internal.Document/LCU.API.Internal.Document.csproj ./source/LCU.API.Internal.Document/
COPY LCU.API.Internal.DocumentLib/LCU.API.Internal.DocumentLib.csproj ./source/LCU.API.Internal.DocumentLib/
COPY LCU.Library.Internal.DocumentModels/LCU.Library.Internal.DocumentModels.csproj ./source/LCU.Library.Internal.DocumentModels/
# Install Credential Provider and set env variables to enable Nuget restore with auth
ARG PAT=bh2lq3jibktw67b2wdbqyfln7cd3je6lk52kijntpj43v6kimpgq
RUN curl -L https://raw.githubusercontent.com/Microsoft/artifacts-credprovider/master/helpers/installcredprovider.sh | sh
ENV NUGET_CREDENTIALPROVIDER_SESSIONTOKENCACHE_ENABLED true
ENV VSS_NUGET_EXTERNAL_FEED_ENDPOINTS "{\"endpointCredentials\": [{\"endpoint\":\"https://landmarkcu.pkgs.visualstudio.com/_packaging/LCU-Internal/nuget/v3/index.json\", \"password\":\"${PAT}\"}]}"
RUN dotnet restore -s "https://landmarkcu.pkgs.visualstudio.com/_packaging/LCU-Internal/nuget/v3/index.json" -s "https://api.nuget.org/v3/index.json" ./source/LCU.API.Internal.Document/LCU.API.Internal.Document.csproj
# Copy All Files
COPY . ./source/
LABEL test=true
COPY LCU.API.Internal.DocumentTest/LCU.API.Internal.DocumentTest.csproj ./source/LCU.API.Internal.DocumentTest/
RUN dotnet restore ./source/LCU.API.Internal.DocumentTest/LCU.API.Internal.DocumentTest.csproj
RUN dotnet test -c Release --results-directory /testresults --logger "trx;LogFileName=test_results.trx" ./source/LCU.API.Internal.DocumentTest/LCU.API.Internal.DocumentTest.csproj
# .NET Core Publish
RUN dotnet publish ./source/LCU.API.Internal.Document/LCU.API.Internal.Document.csproj -c Release -o /dist
# ASP.NET Core
FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine3.20
RUN apk add --no-cache icu-libs && apk add --no-cache tzdata;
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false
ENV ASPNETCORE_ENVIRONMENT=Development
WORKDIR /app
COPY --from=aspnetcore-build /dist .
ENTRYPOINT ["dotnet", "LCU.API.Internal.Document.dll"]
@@ -0,0 +1,11 @@
namespace LCU.API.Internal.Document.PerformanceTest.API;
public static class ApiFactory
{
public static IDocumentApi Api => Environment.Type switch
{
EnvironmentType.Local => new DirectInMemoryDocumentApi(),
EnvironmentType.Azure => new DirectInMemoryDocumentApi(),
_ => throw new NotImplementedException()
};
}
@@ -0,0 +1,49 @@
using LCU.API.Internal.Document.Configurations;
using LCU.API.Internal.Document.Controllers;
using LCU.API.Internal.Document.PerformanceTest.DB;
using LCU.API.Internal.DocumentLib.Configurations;
using LCU.API.Internal.DocumentLib.DataAccess;
using LCU.Library.Internal.DocumentModels.Requests.Search;
using LCU.Library.Internal.DocumentModels.Responses;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Newtonsoft.Json;
namespace LCU.API.Internal.Document.PerformanceTest.API;
public class DirectInMemoryDocumentApi : IDocumentApi
{
private IServiceProvider? _services;
public Task SpinUp(DatabaseConfiguration databaseConfiguration)
{
if (_services is not null) return Task.CompletedTask;
var services = new ServiceCollection();
ServiceRegistrations.AddDocumentServices(services);
ServiceRegistrations.AddDocumentDataAccesses(services);
ServiceRegistrations.AddDocumentRepositories(services);
services.AddTransient<DocumentController>();
services.TryAddSingleton(databaseConfiguration);
services.AddSingleton<IBlobStorageClient, NullBlobStorageClient>();
services.AddSingleton<ApiConfiguration>();
services.AddSingleton<NotificationApiConfiguration>();
_services = services.BuildServiceProvider();
return Task.CompletedTask;
}
public async Task<IList<DocumentInformationResponse>> GetDocumentInformationByFilter(DocumentInformationFilter filter)
{
var controller = _services?.GetRequiredService<DocumentController>() ?? throw new InvalidOperationException();
var response = await controller.PostDocumentInformationByFilter(new DocumentInformationFilteredSearch{ Filter = filter });
return (response as OkObjectResult)?.Value as IList<DocumentInformationResponse> ?? throw new InvalidOperationException();
}
public Task TearDown()
{
return Task.CompletedTask;
}
}
@@ -0,0 +1,12 @@
using LCU.API.Internal.DocumentLib.Configurations;
using LCU.Library.Internal.DocumentModels.Requests.Search;
using LCU.Library.Internal.DocumentModels.Responses;
namespace LCU.API.Internal.Document.PerformanceTest.API;
public interface IDocumentApi
{
Task SpinUp(DatabaseConfiguration databaseConfiguration);
Task<IList<DocumentInformationResponse>> GetDocumentInformationByFilter(DocumentInformationFilter filter);
Task TearDown();
}
@@ -0,0 +1,99 @@
using Azure;
using Azure.Core;
using Azure.Identity;
using Azure.ResourceManager;
using Azure.ResourceManager.CosmosDB;
using Azure.ResourceManager.CosmosDB.Models;
using Azure.ResourceManager.Resources;
using MongoDB.Driver;
namespace LCU.API.Internal.Document.PerformanceTest.DB;
public class AzureCosmosDb : IDatabase
{
private readonly string _performanceTestingResourceName = "Perf_MongoDb";
public async Task<IMongoClient> SpinUp()
{
Console.WriteLine("Currently in Azure Spin Up.");
//todo test this - cd: I'm not connected to the landmark subscription in the portal, only AD
var azureClient = new ArmClient(new DefaultAzureCredential());
var subscription = await azureClient.GetDefaultSubscriptionAsync();
var resourceGroup = (
await subscription
.GetResourceGroups()
.CreateOrUpdateAsync
(
WaitUntil.Completed,
_performanceTestingResourceName,
new ResourceGroupData(AzureLocation.CentralUS)
)
)
.Value;
var cosmosAccount = (
await resourceGroup
.GetCosmosDBAccounts()
.CreateOrUpdateAsync
(
WaitUntil.Completed,
_performanceTestingResourceName.ToLower().Replace("_",""),
new CosmosDBAccountCreateOrUpdateContent
(
AzureLocation.CentralUS,
new CosmosDBAccountLocation[] {
new CosmosDBAccountLocation {
LocationName = AzureLocation.CentralUS,
FailoverPriority = 0,
IsZoneRedundant = false
},
new CosmosDBAccountLocation {
LocationName = AzureLocation.EastUS2,
FailoverPriority = 1,
IsZoneRedundant = false
}
}
)
{ Kind = "MongoDB"}
)
)
.Value;
var mongoDb = cosmosAccount.GetMongoDBDatabases();
await mongoDb
.CreateOrUpdateAsync
(
WaitUntil.Completed,
_performanceTestingResourceName,
new MongoDBDatabaseCreateOrUpdateContent
(
AzureLocation.CentralUS,
new MongoDBDatabaseResourceInfo(_performanceTestingResourceName)
)
);
//todo test to ensure that there's only one connection string or that it can be determined which is the one we need for mongo
var allConnections = cosmosAccount
.GetConnectionStrings()
.AsPages()
.ToArray()
.SelectMany(p => p.Values)
.ToArray();
if (allConnections.Length == 0) throw new Exception("No Connection Strings Found.");
_connectionString = cosmosAccount.GetConnectionStrings().AsPages().ToArray()[0].Values[0].ConnectionString;
return new MongoClient(ConnectionString);
}
public async Task TearDown()
{
var azureClient = new ArmClient(new DefaultAzureCredential());
var subscription = await azureClient.GetDefaultSubscriptionAsync();
var resourceGroup = (await subscription.GetResourceGroupAsync(_performanceTestingResourceName)).Value;
await resourceGroup.DeleteAsync(WaitUntil.Completed);
}
public string ConnectionString => _connectionString ?? throw new InvalidOperationException("spin up the database first");
private string? _connectionString;
}
@@ -0,0 +1,11 @@
namespace LCU.API.Internal.Document.PerformanceTest.DB;
public static class DatabaseFactory
{
public static IDatabase Database => Environment.Type switch
{
EnvironmentType.Local => new LocalMongoDatabase(),
EnvironmentType.Azure => new AzureCosmosDb(),
_ => throw new NotImplementedException()
};
}
@@ -0,0 +1,10 @@
using MongoDB.Driver;
namespace LCU.API.Internal.Document.PerformanceTest.DB;
public interface IDatabase
{
Task<IMongoClient> SpinUp();
Task TearDown();
string ConnectionString { get; }
}
@@ -0,0 +1,26 @@
using Mongo2Go;
using MongoDB.Driver;
namespace LCU.API.Internal.Document.PerformanceTest.DB;
public class LocalMongoDatabase : IDatabase
{
private MongoDbRunner? _mongoDb;
public Task<IMongoClient> SpinUp()
{
_mongoDb ??= MongoDbRunner.Start();
return Task.FromResult<IMongoClient>(new MongoClient(ConnectionString));
}
public Task TearDown()
{
_mongoDb?.Dispose();
_mongoDb = null;
return Task.CompletedTask;
}
public string ConnectionString => _mongoDb?.ConnectionString ?? throw new InvalidOperationException("spin up the database first");
}
@@ -0,0 +1,9 @@
using Azure.Storage.Blobs;
using LCU.API.Internal.DocumentLib.DataAccess;
namespace LCU.API.Internal.Document.PerformanceTest.DB;
public class NullBlobStorageClient : IBlobStorageClient
{
public BlobContainerClient GetClient() => null!;
}
@@ -0,0 +1,12 @@
namespace LCU.API.Internal.Document.PerformanceTest;
public enum EnvironmentType
{
Local,
Azure
}
public static class Environment
{
public static EnvironmentType Type { get; set; } = EnvironmentType.Local;
}
@@ -0,0 +1,120 @@
using BenchmarkDotNet.Attributes;
using LCU.API.Internal.Document.PerformanceTest.API;
using LCU.API.Internal.Document.PerformanceTest.DB;
using LCU.API.Internal.DocumentLib.Configurations;
using LCU.API.Internal.DocumentLib.Containers;
using LCU.Library.Internal.DocumentModels.Requests.Search;
using MongoDB.Driver;
namespace LCU.API.Internal.Document.PerformanceTest;
[RPlotExporter]
public class GetDocumentInformationByFilterLoadTest
{
static GetDocumentInformationByFilterLoadTest()
{
Environment.Type = EnvironmentType.Local;
}
//[Params(1, 10, 100, 1000, 10000, 100000, 250000, 500000, 750000, 1000000, 2000000)]
[Params(1, 10, 100, 1000, 10000, 100000, 250000, 500000, 750000, 1000000)]
public int DocumentCount { get; set; }
private DocumentInformation _document = null!;
private readonly DatabaseConfiguration _config;
private readonly IDatabase _database = DatabaseFactory.Database;
private readonly IDocumentApi _api = ApiFactory.Api;
private IMongoClient _client = null!;
public GetDocumentInformationByFilterLoadTest()
{
_config = new DatabaseConfiguration
{
DatabaseName = $"{DocumentCount}Documents",
DocumentContainerName = "Documents",
AuditContainerName = "Audits"
};
}
[GlobalSetup]
public void GlobalSetup()
{
_client = _database.SpinUp().Result;
_config.ConnectionString = _database.ConnectionString;
var db = _client.GetDatabase(_config.DatabaseName);
db.CreateCollection(_config.DocumentContainerName);
db.CreateCollection(_config.AuditContainerName);
var documentCollection = db.GetCollection<DocumentInformation>(_config.DocumentContainerName);
Task.WaitAll
(
_api.SpinUp(_config),
documentCollection.InsertManyAsync(Enumerable.Repeat(() => DocumentFactory(), DocumentCount).Select(f => f()))
);
}
[IterationSetup]
public void Setup()
{
_document = DocumentFactory();
_client.GetDatabase(_config.DatabaseName)
.GetCollection<DocumentInformation>(_config.DocumentContainerName)
.InsertOne(_document);
}
[Benchmark]
public void GetDocumentInformationByFilterRun()
{
var oneThirdAttributes = _document.Attributes.Count / 3;
var filter = new DocumentInformationFilter
{
AttributesContains = _document.Attributes.ToArray()[..oneThirdAttributes].ToDictionary(p => p.Key, p => p.Value),
AttributesContainsAny = _document.Attributes.ToArray()[oneThirdAttributes..^oneThirdAttributes].ToDictionary(p => p.Key, p => p.Value),
AttributesEquals = _document.Attributes.ToArray()[^oneThirdAttributes..].ToDictionary(p => p.Key, p => p.Value),
DataSource = _document.DataSource,
Name = _document.Name,
Type = _document.Type,
Source = _document.Source,
ScanStatus = _document.ScanStatus,
};
_api.GetDocumentInformationByFilter(filter).Wait();
}
[GlobalCleanup]
public void GlobalCleanup()
{
_client.DropDatabase(_config.DatabaseName);
var dbDestruction = _database.TearDown();
var apiDestruction = _api.TearDown();
Task.WaitAll(dbDestruction, apiDestruction);
}
private static readonly Random Random = new Random(DateTime.Now.Millisecond);
private static DocumentInformation DocumentFactory() => new()
{
Name = Guid.NewGuid().ToString(),
Type = Random.Next().ToString(),
Source = Random.Next().ToString(),
DataSource = Random.Next().ToString(),
FileSize = Random.Next(),
ScanStatus = Random.Next() % 2 == 0,
UploadDate = DateTime.Now.AddDays(-Random.Next(1, 10)),
LastModifiedDate = DateTime.Now.AddDays(-Random.Next(1, 10)),
CreatedDate = DateTime.Now.AddDays(-Random.Next(1, 10)),
Attributes = Enumerable
.Repeat
(
() => new KeyValuePair<string, string>(Guid.NewGuid().ToString(), Random.Next().ToString()),
Random.Next(10, 100)
)
.Select(f => f())
.ToDictionary(p => p.Key, p => p.Value)
};
}
@@ -0,0 +1,130 @@
using System.Configuration;
using Azure.Identity;
using BenchmarkDotNet.Attributes;
using LCU.API.Internal.Document.PerformanceTest.API;
using LCU.API.Internal.DocumentLib.Configurations;
using LCU.API.Internal.DocumentLib.Containers;
using LCU.Library.Internal.DocumentModels.Requests.Search;
using Microsoft.Extensions.Configuration;
using MongoDB.Driver;
namespace LCU.API.Internal.Document.PerformanceTest;
public enum Indexing
{
Default,
FieldSpecific,
Wildcard
}
//todo it would be great to get the RU cost output
[RPlotExporter]
[ShortRunJob]
public class IndexingStrategyPerformanceTest
{
[Params(Indexing.Default, Indexing.FieldSpecific, Indexing.Wildcard)]
public Indexing Strategy { get; set; }
private readonly IDocumentApi _api = ApiFactory.Api;
private readonly DatabaseConfiguration _config = new();
public IndexingStrategyPerformanceTest()
{
var configBuilder = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.AddJsonFile("appsettings.Development.json");
var baseConfig = configBuilder.Build();
var clientSecretCredentials = new ClientSecretCredential
(
baseConfig["TenantId"],
baseConfig["ClientId"],
baseConfig["ClientSecret"]
);
var vaultLocation = new Uri($"https://{baseConfig["AzureKeyVaultUrl"]}.vault.azure.net/");
configBuilder
.AddAzureKeyVault(vaultLocation, clientSecretCredentials)
.Build()
.Bind("MongoDatabase", _config);
}
[GlobalSetup]
public void GlobalSetup()
{
switch (Strategy)
{
case Indexing.Default:
break;
case Indexing.FieldSpecific:
new MongoClient(_config.ConnectionString)
.GetDatabase(_config.DatabaseName)
.GetCollection<DocumentInformation>(_config.DocumentContainerName)
.Indexes
.CreateOne
(
new CreateIndexModel<DocumentInformation>
(
Builders<DocumentInformation>.IndexKeys.Ascending(d => d.Name),
new CreateIndexOptions { Name = nameof(DocumentInformation.Name) }
)
);
break;
case Indexing.Wildcard:
new MongoClient(_config.ConnectionString)
.GetDatabase(_config.DatabaseName)
.GetCollection<DocumentInformation>(_config.DocumentContainerName)
.Indexes
.CreateOne
(
new CreateIndexModel<DocumentInformation>
(
Builders<DocumentInformation>.IndexKeys.Wildcard(),
new CreateIndexOptions { Name = "wildcard" }
)
);
break;
default:
throw new ArgumentOutOfRangeException();
}
_api.SpinUp(_config).Wait();
}
[Benchmark]
//todo report the RU cost in the benchmark
public async Task DocumentSearch_ByName()
{
var filter = new DocumentInformationFilter
{
Name = "5498-SA"
};
await _api.GetDocumentInformationByFilter(filter);
}
[GlobalCleanup]
public void GlobalCleanup()
{
switch (Strategy)
{
case Indexing.Default:
break;
case Indexing.FieldSpecific:
new MongoClient(_config.ConnectionString)
.GetDatabase(_config.DatabaseName)
.GetCollection<DocumentInformation>(_config.DocumentContainerName)
.Indexes
.DropOne(nameof(DocumentInformation.Name));
break;
case Indexing.Wildcard:
new MongoClient(_config.ConnectionString)
.GetDatabase(_config.DatabaseName)
.GetCollection<DocumentInformation>(_config.DocumentContainerName)
.Indexes
.DropOne("wildcard");
break;
default:
throw new ArgumentOutOfRangeException();
}
}
}
@@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.ResourceManager" Version="1.3.1" />
<PackageReference Include="Azure.ResourceManager.CosmosDB" Version="1.0.0" />
<PackageReference Include="BenchmarkDotNet" Version="0.13.2" />
<PackageReference Include="Mongo2Go" Version="3.1.3" />
</ItemGroup>
<ItemGroup>
<Content Include="..\LCU.API.Internal.Document\appsettings.json" Link="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\LCU.API.Internal.DocumentLib\LCU.API.Internal.DocumentLib.csproj" />
<ProjectReference Include="..\LCU.API.Internal.Document\LCU.API.Internal.Document.csproj" />
<ProjectReference Include="..\LCU.Library.Internal.DocumentModels\LCU.Library.Internal.DocumentModels.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,5 @@
using BenchmarkDotNet.Running;
using LCU.API.Internal.Document.PerformanceTest;
//BenchmarkRunner.Run<GetDocumentInformationByFilterLoadTest>();
BenchmarkRunner.Run<IndexingStrategyPerformanceTest>();
@@ -0,0 +1,122 @@
# Running Benchmarks
`dotnet run -c Release`
## past results
### 2022.09.09 - local mongodb - "old" regex matching
note: default mongodb indexing is not the same as CosmosDB Mongo default indexing
``` ini
BenchmarkDotNet=v0.13.2, OS=Windows 10 (10.0.19042.1826/20H2/October2020Update)
11th Gen Intel Core i5-1145G7 2.60GHz, 1 CPU, 8 logical and 4 physical cores
.NET SDK=6.0.400
[Host] : .NET 6.0.8 (6.0.822.36306), X64 RyuJIT AVX2
Job-HPFGMU : .NET 6.0.8 (6.0.822.36306), X64 RyuJIT AVX2
InvocationCount=1 UnrollFactor=1
```
| Method | DocumentCount | Mean | Error | StdDev |
|---------------------------------- |-------------- |--------------:|--------------:|--------------:|
| **GetDocumentInformationByFilterRun** | **1** | **3.756 ms** | **0.6389 ms** | **1.874 ms** |
| **GetDocumentInformationByFilterRun** | **10** | **3.868 ms** | **0.6174 ms** | **1.820 ms** |
| **GetDocumentInformationByFilterRun** | **100** | **8.027 ms** | **1.0094 ms** | **2.976 ms** |
| **GetDocumentInformationByFilterRun** | **1000** | **41.938 ms** | **7.9785 ms** | **23.525 ms** |
| **GetDocumentInformationByFilterRun** | **10000** | **337.205 ms** | **51.8841 ms** | **152.167 ms** |
| **GetDocumentInformationByFilterRun** | **100000** | **3,429.605 ms** | **632.3863 ms** | **1,864.605 ms** |
| **GetDocumentInformationByFilterRun** | **250000** | **7,896.811 ms** | **1,431.8274 ms** | **4,221.775 ms** |
| **GetDocumentInformationByFilterRun** | **500000** | **16,629.300 ms** | **2,749.5569 ms** | **8,107.131 ms** |
| **GetDocumentInformationByFilterRun** | **750000** | **23,883.445 ms** | **4,083.3474 ms** | **11,911.316 ms** |
| **GetDocumentInformationByFilterRun** | **1000000** | **32,725.371 ms** | **5,751.8744 ms** | **16,778.487 ms** |
![](./ResultPlots/2022.09.12-localmongodb-oldregex-GetDocumentInformationByFilterLoadTest-boxplot.png =500x)
### 2022.09.12 - local mongodb - "new" regex matching
``` ini
BenchmarkDotNet=v0.13.2, OS=Windows 10 (10.0.19042.1826/20H2/October2020Update)
11th Gen Intel Core i5-1145G7 2.60GHz, 1 CPU, 8 logical and 4 physical cores
.NET SDK=6.0.400
[Host] : .NET 6.0.8 (6.0.822.36306), X64 RyuJIT AVX2
Job-ZGOEWK : .NET 6.0.8 (6.0.822.36306), X64 RyuJIT AVX2
InvocationCount=1 UnrollFactor=1
```
| Method | DocumentCount | Mean | Error | StdDev | Median |
|---------------------------------- |-------------- |--------------:|--------------:|--------------:|--------------:|
| **GetDocumentInformationByFilterRun** | **1** | **3.924 ms** | **0.6009 ms** | **1.724 ms** | **3.724 ms** |
| **GetDocumentInformationByFilterRun** | **10** | **3.675 ms** | **0.6997 ms** | **2.063 ms** | **3.096 ms** |
| **GetDocumentInformationByFilterRun** | **100** | **5.679 ms** | **0.8358 ms** | **2.464 ms** | **5.332 ms** |
| **GetDocumentInformationByFilterRun** | **1000** | **35.936 ms** | **5.6629 ms** | **16.697 ms** | **37.860 ms** |
| **GetDocumentInformationByFilterRun** | **10000** | **305.437 ms** | **53.4368 ms** | **157.560 ms** | **291.339 ms** |
| **GetDocumentInformationByFilterRun** | **100000** | **2,946.267 ms** | **483.7081 ms** | **1,418.632 ms** | **2,793.991 ms** |
| **GetDocumentInformationByFilterRun** | **250000** | **8,456.390 ms** | **1,315.9931 ms** | **3,796.941 ms** | **8,575.654 ms** |
| **GetDocumentInformationByFilterRun** | **500000** | **15,775.558 ms** | **2,830.8954 ms** | **8,167.781 ms** | **15,527.663 ms** |
| **GetDocumentInformationByFilterRun** | **750000** | **24,948.355 ms** | **3,861.0450 ms** | **11,323.777 ms** | **25,630.684 ms** |
| **GetDocumentInformationByFilterRun** | **1000000** | **30,470.661 ms** | **5,342.0572 ms** | **15,583.032 ms** | **29,073.994 ms** |
![](./ResultPlots/2022.09.12-localmongodb-newregex-GetDocumentInformationByFilterLoadTest-boxplot.png =500x)
### 2022.09.28 - indexing strategies
``` ini
BenchmarkDotNet=v0.13.2, OS=Windows 10 (10.0.19042.1826/20H2/October2020Update)
11th Gen Intel Core i5-1145G7 2.60GHz, 1 CPU, 8 logical and 4 physical cores
.NET SDK=6.0.400
[Host] : .NET 6.0.8 (6.0.822.36306), X64 RyuJIT AVX2
ShortRun : .NET 6.0.8 (6.0.822.36306), X64 RyuJIT AVX2
Job=ShortRun IterationCount=3 LaunchCount=1
WarmupCount=3
```
| Method | Strategy | Mean | Error | StdDev |
|---------------------- |-------------- |----------:|---------:|---------:|
| **DocumentSearch_ByName** | **Default** | **171.51 ms** | **68.40 ms** | **3.749 ms** |
| **DocumentSearch_ByName** | **FieldSpecific** | **91.36 ms** | **29.19 ms** | **1.600 ms** |
| **DocumentSearch_ByName** | **Wildcard** | **94.74 ms** | **78.58 ms** | **4.307 ms** |
![](/ResultPlots/2022.09-28-LCU.API.Internal.Document.PerformanceTest.IndexingStrategyPerformanceTest-boxplot.png)
## required tools
- dotnet 6+
-- `choco install dotnet`
-- probably installed with visual studio
- R
-- `choco install r.project` then add "C:\Program Files\R\R-4.2.1\bin" to your path, assuming you used the default install directory and the version is 4.2.1
--- better to manually add to the path because setx will only give you 1024 characters. Your path is likely already longer than that and will get truncated.
-- this is for outputting visual graphs, not just text tables into standard out
## output
- written to the console
- `.\BenchmarkDotNet.Artifacts` relative to where ever you ran the code from
### graphs
The plots should be produced automatically and you'll find them in the results folder.
If not you can manually generate them:
- `cd .\BenchmarkDotNet.Artifacts\results`
- `rscript BuildPlots.R`
## benchmarkdotnet docs
https://benchmarkdotnet.org/
## gotchas
### Debugging
This will be more difficult than you might be accustomed to. By default benchmark dotnet takes control of a lot of parts of the runtime and even rebuilds some parts of the assembly to ensure isolation and consistent optimzation. See their docs for enabling debugging.
It's possible to run or attach to a release optimized assembly. Some of the things you might expect to be there won't be availabe to the debugger; you're missing debug symbols and the IL has been optimized.
There's always good old console output and file logging.
You might also want to try setting up the problematic scenario in a unit test and then debug *that* instead of debugging the benchmark itself.
### Singletons
Benchmarkdotnet ends up invoking msbuild in order to build separate assemblies (for controlling optimization and isolation). This means that, e.g. `Environment.Type = EnvironmentType.Azure;` in program.cs will not affect the benchmarks when they are run; Environment.Type will be recreated every benchmark because benchmarks are running in a different assembly than the `BenchmarkRunner.Run<T>();` statement which caused the benchmark to run.
One option to deal with this is setting the option inside the benchmark class e.g. in a static constructor (ensuring the value is set before anything else). There are almost surely other options, perhaps a way to pass arguments (other than `[Params()]`) to the benchmark class.
### Azure Rate Limiting
Despite having an Azure environment with code to spin it up and tear it down trying to run a benchmark against a real Azure CosmosDB will likely run you into rate limiting issues. Specifically inserting enough documents to produce a notable database load in a short amount of time. As of 2022.09.13 the rate limit is 1000 records per 5 minutes... You can see where this might produce a problem trying to produce a database on the order of hundreds of thousands of records.
See https://docs.microsoft.com/en-us/azure/azure-resource-manager/management/request-limits-and-throttling#network-throttling for current information.
@@ -0,0 +1,87 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.32112.339
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LCU.API.Internal.Document", "LCU.API.Internal.Document\LCU.API.Internal.Document.csproj", "{424A4E9B-D5C8-4ABF-A073-30CCE4759E57}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LCU.API.Internal.DocumentTest", "LCU.API.Internal.DocumentTest\LCU.API.Internal.DocumentTest.csproj", "{AB05AA6E-F86D-45D0-BF61-4536D6EE6495}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LCU.Library.Internal.DocumentModels", "LCU.Library.Internal.DocumentModels\LCU.Library.Internal.DocumentModels.csproj", "{8D9A978B-DEA8-41A7-8F52-BE0A78E08511}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LCU.API.Internal.DocumentLib", "LCU.API.Internal.DocumentLib\LCU.API.Internal.DocumentLib.csproj", "{A4C80FE4-0880-4884-ABDD-88DA6DDA9EC5}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LCU.API.Internal.Document.PerformanceTest", "LCU.API.Internal.Document.PerformanceTest\LCU.API.Internal.Document.PerformanceTest.csproj", "{9E11130D-8E7A-4A3A-8128-64B2FDDF214B}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Migrations", "Migrations", "{410AA7F0-3C37-4F31-BA3D-2FF85163FB41}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OwnerNumberMigration", "OwnerNumberMigration\OwnerNumberMigration.csproj", "{00B4C125-83E7-4375-9F57-5D5BE17E24B2}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OwnerNumberMigrationTest", "OwnerNumberMigrationTest\OwnerNumberMigrationTest.csproj", "{F21E3FE1-4587-4FE7-ACDD-7ABA5E6FA121}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{25142763-0DF7-4898-A4C6-395DAF0456F9}"
ProjectSection(SolutionItems) = preProject
.gitignore = .gitignore
azure-pipelines-iis.yml = azure-pipelines-iis.yml
azure-pipelines.yml = azure-pipelines.yml
README.md = README.md
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LCU.API.Internal.DocumentLibTest", "LCU.API.Internal.DocumentLibTest\LCU.API.Internal.DocumentLibTest.csproj", "{7755B890-92CC-4811-861F-6C99ABB210CE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LCU.Library.Internal.DocumentModelsTest", "LCU.Library.Internal.DocumentModelsTest\LCU.Library.Internal.DocumentModelsTest.csproj", "{82F351A4-6C78-493D-A76A-5DF57CAC709D}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{424A4E9B-D5C8-4ABF-A073-30CCE4759E57}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{424A4E9B-D5C8-4ABF-A073-30CCE4759E57}.Debug|Any CPU.Build.0 = Debug|Any CPU
{424A4E9B-D5C8-4ABF-A073-30CCE4759E57}.Release|Any CPU.ActiveCfg = Release|Any CPU
{424A4E9B-D5C8-4ABF-A073-30CCE4759E57}.Release|Any CPU.Build.0 = Release|Any CPU
{AB05AA6E-F86D-45D0-BF61-4536D6EE6495}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AB05AA6E-F86D-45D0-BF61-4536D6EE6495}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AB05AA6E-F86D-45D0-BF61-4536D6EE6495}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AB05AA6E-F86D-45D0-BF61-4536D6EE6495}.Release|Any CPU.Build.0 = Release|Any CPU
{8D9A978B-DEA8-41A7-8F52-BE0A78E08511}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8D9A978B-DEA8-41A7-8F52-BE0A78E08511}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8D9A978B-DEA8-41A7-8F52-BE0A78E08511}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8D9A978B-DEA8-41A7-8F52-BE0A78E08511}.Release|Any CPU.Build.0 = Release|Any CPU
{A4C80FE4-0880-4884-ABDD-88DA6DDA9EC5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A4C80FE4-0880-4884-ABDD-88DA6DDA9EC5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A4C80FE4-0880-4884-ABDD-88DA6DDA9EC5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A4C80FE4-0880-4884-ABDD-88DA6DDA9EC5}.Release|Any CPU.Build.0 = Release|Any CPU
{9E11130D-8E7A-4A3A-8128-64B2FDDF214B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9E11130D-8E7A-4A3A-8128-64B2FDDF214B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9E11130D-8E7A-4A3A-8128-64B2FDDF214B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9E11130D-8E7A-4A3A-8128-64B2FDDF214B}.Release|Any CPU.Build.0 = Release|Any CPU
{00B4C125-83E7-4375-9F57-5D5BE17E24B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{00B4C125-83E7-4375-9F57-5D5BE17E24B2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{00B4C125-83E7-4375-9F57-5D5BE17E24B2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{00B4C125-83E7-4375-9F57-5D5BE17E24B2}.Release|Any CPU.Build.0 = Release|Any CPU
{F21E3FE1-4587-4FE7-ACDD-7ABA5E6FA121}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F21E3FE1-4587-4FE7-ACDD-7ABA5E6FA121}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F21E3FE1-4587-4FE7-ACDD-7ABA5E6FA121}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F21E3FE1-4587-4FE7-ACDD-7ABA5E6FA121}.Release|Any CPU.Build.0 = Release|Any CPU
{7755B890-92CC-4811-861F-6C99ABB210CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7755B890-92CC-4811-861F-6C99ABB210CE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7755B890-92CC-4811-861F-6C99ABB210CE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7755B890-92CC-4811-861F-6C99ABB210CE}.Release|Any CPU.Build.0 = Release|Any CPU
{82F351A4-6C78-493D-A76A-5DF57CAC709D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{82F351A4-6C78-493D-A76A-5DF57CAC709D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{82F351A4-6C78-493D-A76A-5DF57CAC709D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{82F351A4-6C78-493D-A76A-5DF57CAC709D}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{00B4C125-83E7-4375-9F57-5D5BE17E24B2} = {410AA7F0-3C37-4F31-BA3D-2FF85163FB41}
{F21E3FE1-4587-4FE7-ACDD-7ABA5E6FA121} = {410AA7F0-3C37-4F31-BA3D-2FF85163FB41}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {AB145BEF-D8D6-4CB9-BE2C-79F06F523B0D}
EndGlobalSection
EndGlobal
@@ -0,0 +1,12 @@
namespace LCU.API.Internal.Document.Configurations;
public class ApiConfiguration
{
public string AppName { get; set; }
public string SeqUrl { get; set; }
public string SeqApiKey { get; set; }
public int FileUploadCutoffHours { get; set; }
public string[] CorsAllowedOrigins { get; set; }
public List<string> MaliciousFileFoundEmailList { get; set; }
public bool IsAksDeploy { get; set; }
}
@@ -0,0 +1,588 @@
using LCU.API.Internal.Document.Services;
using LCU.Library.Internal.ApiSecurity.Attributes;
using LCU.Library.Internal.DocumentModels.Requests;
using LCU.Library.Internal.DocumentModels.Requests.Search;
using LCU.Library.Internal.DocumentModels.Responses;
using LCU.Library.Internal.Exceptions;
using Microsoft.AspNetCore.Mvc;
using Serilog;
using Microsoft.AspNetCore.Authorization;
namespace LCU.API.Internal.Document.Controllers;
[ApiController]
[Route("[controller]")]
public class DocumentController : ControllerBase
{
private readonly IDocumentService _documentService;
private readonly IDocumentAuditService _documentAuditService;
private readonly List<DocumentAction> _allowedExternalActions = new()
{
DocumentAction.Download
};
public DocumentController(IDocumentService documentService,
IDocumentAuditService documentAuditService)
{
_documentService = documentService;
_documentAuditService = documentAuditService;
}
[ApiKey]
[HttpGet("{documentId:guid}")]
[ProducesResponseType(typeof(DocumentResponse), 200)]
[ProducesResponseType(401)]
[ProducesResponseType(404)]
[ProducesResponseType(500)]
public async Task<IActionResult> GetDocumentById(Guid documentId)
{
try
{
var result = await _documentService.GetDocument(documentId);
if (result == null)
{
return NotFound($"The document with DocumentId:{documentId} could not be found.");
}
return Ok(result);
}
catch (ServiceException ex)
{
return StatusCode(StatusCodes.Status500InternalServerError, ex.Message);
}
catch (Exception exception)
{
Log.Error(exception, $"Unexpected Exception in {GetType().Name}");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[ApiKey]
[HttpGet("{personOrOrgNumber:long}/{documentId:guid}")]
[ProducesResponseType(typeof(DocumentResponse), 200)]
[ProducesResponseType(401)]
[ProducesResponseType(404)]
[ProducesResponseType(500)]
public async Task<IActionResult> GetMemberDocumentById(long personOrOrgNumber, Guid documentId)
{
try
{
var result = await _documentService.GetDocument(personOrOrgNumber, documentId);
if (result == null)
{
return NotFound($"The document with DocumentId:{documentId} could not be found.");
}
return Ok(result);
}
catch (ServiceException ex)
{
return StatusCode(StatusCodes.Status500InternalServerError, ex.Message);
}
catch (Exception exception)
{
Log.Error(exception, $"Unexpected Exception in {GetType().Name}");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[ApiKey]
[Obsolete]
[HttpGet("GetDocumentByBlobId/{documentBlobId:guid}")]
[ProducesResponseType(typeof(byte[]), 200)]
[ProducesResponseType(401)]
[ProducesResponseType(404)]
[ProducesResponseType(500)]
public async Task<IActionResult> GetDocumentByBlobId(Guid documentBlobId)
{
try
{
var result = await _documentService.GetDocumentByBlobId(documentBlobId);
if (result == null)
{
return NotFound($"The document with DocumentBlobId:{documentBlobId} could not be found.");
}
return Ok(result);
}
catch (ServiceException ex)
{
return StatusCode(StatusCodes.Status500InternalServerError, ex.Message);
}
catch (Exception exception)
{
Log.Error(exception, $"Unexpected Exception in {GetType().Name}");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[ApiKey]
[HttpGet("Content/{documentBlobId:guid}")]
[ProducesResponseType(typeof(byte[]), 200)]
[ProducesResponseType(401)]
[ProducesResponseType(404)]
[ProducesResponseType(500)]
public async Task<IActionResult> GetDocumentContentByBlobId(Guid documentBlobId)
{
try
{
var result = await _documentService.GetDocumentByBlobId(documentBlobId);
if (result == null)
{
return NotFound($"The document with DocumentBlobId:{documentBlobId} could not be found.");
}
return Ok(result);
}
catch (ServiceException ex)
{
return StatusCode(StatusCodes.Status500InternalServerError, ex.Message);
}
catch (Exception exception)
{
Log.Error(exception, $"Unexpected Exception in {GetType().Name}");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[ApiKey]
[HttpGet("{personOrOrgNumber:long}/Content/{documentBlobId:guid}")]
[ProducesResponseType(typeof(byte[]), 200)]
[ProducesResponseType(401)]
[ProducesResponseType(404)]
[ProducesResponseType(500)]
public async Task<IActionResult> GetMemberDocumentContentByBlobId(long personOrOrgNumber, Guid documentBlobId)
{
try
{
var result = await _documentService.GetDocumentByBlobId(personOrOrgNumber, documentBlobId);
if (result == null)
{
return NotFound($"The document with DocumentBlobId:{documentBlobId} could not be found.");
}
return Ok(result);
}
catch (ServiceException ex)
{
return StatusCode(StatusCodes.Status500InternalServerError, ex.Message);
}
catch (Exception exception)
{
Log.Error(exception, $"Unexpected Exception in {GetType().Name}");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[ApiKey]
[HttpPost("Search")]
[ProducesResponseType(typeof(List<DocumentInformationResponse>),200)]
[ProducesResponseType(204)]
[ProducesResponseType(401)]
[ProducesResponseType(500)]
public async Task<IActionResult> PostDocumentInformationByFilter([FromBody] DocumentInformationFilteredSearch documentInformationFilteredSearch)
{
try
{
var result = await _documentService.SearchDocumentInformation(documentInformationFilteredSearch.Filter, documentInformationFilteredSearch.Skip, documentInformationFilteredSearch.Take, documentInformationFilteredSearch.SortDefinition);
if (!result.Any())
{
return NoContent();
}
return Ok(result);
}
catch (ServiceException ex)
{
return StatusCode(StatusCodes.Status500InternalServerError, ex.Message);
}
catch (Exception exception)
{
Log.Error(exception, $"Unexpected Exception in {GetType().Name}");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[ApiKey]
[HttpGet("AuditInformation/{documentId:guid}")]
[ProducesResponseType(typeof(List<DocumentAuditResponse>),200)]
[ProducesResponseType(204)]
[ProducesResponseType(401)]
[ProducesResponseType(500)]
public async Task<IActionResult> GetDocumentAuditInformationById(Guid documentId)
{
try
{
var result = await _documentAuditService.GetDocumentAudits(documentId);
if (!result.Any())
{
return NoContent();
}
return Ok(result);
}
catch (ServiceException ex)
{
return StatusCode(StatusCodes.Status500InternalServerError, ex.Message);
}
catch (Exception exception)
{
Log.Error(exception, $"Unexpected Exception in {GetType().Name}");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[ApiKey]
[HttpGet("AuditInformation")]
[ProducesResponseType(typeof(List<DocumentAuditResponse>), 200)]
[ProducesResponseType(204)]
[ProducesResponseType(401)]
[ProducesResponseType(500)]
public async Task<IActionResult> GetDocumentAuditInformationByIds([FromQuery]Guid[] documentIds)
{
try
{
var result = await _documentAuditService.GetDocumentAudits(documentIds);
return Ok(result);
}
catch (ServiceException ex)
{
return StatusCode(StatusCodes.Status500InternalServerError, ex.Message);
}
catch (Exception exception)
{
Log.Error(exception, $"Unexpected Exception in {GetType().Name}");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[ApiKey]
[Authorize]
[HttpPost("{documentId:guid}/Actions")]
[ProducesResponseType(200)]
[ProducesResponseType(404)]
[ProducesResponseType(500)]
public async Task<IActionResult> ExternalActionPerformed([FromRoute]Guid documentId, [FromBody] DocumentActionNotificationRequest action)
{
try
{
if (!_allowedExternalActions.Contains(action.Action))
{
throw new BadHttpRequestException($"{action.Action.ToString()} action type not allowed externally.");
}
await _documentAuditService.AuditAction(documentId,
HttpContext.User.Claims.Single(c => c.Type == "name").Value, action.Source, action.Action);
return Ok();
}
catch (BadHttpRequestException ex)
{
return StatusCode(StatusCodes.Status400BadRequest, ex.Message);
}
catch (ServiceException ex)
{
return StatusCode(StatusCodes.Status500InternalServerError, ex.Message);
}
catch (Exception exception)
{
Log.Error(exception, $"Unexpected Exception in {GetType().Name}");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[ApiKey]
[HttpPost]
[ProducesResponseType(201)]
[ProducesResponseType(400)]
[ProducesResponseType(401)]
[ProducesResponseType(500)]
public async Task<IActionResult> UploadDocument([FromBody] DocumentRequest request)
{
try
{
var result = await _documentService.UploadDocument(request);
return CreatedAtAction(nameof(GetDocumentById), new { documentId = result.DocumentId }, result);
}
catch (ServiceException ex)
{
return StatusCode(StatusCodes.Status500InternalServerError, ex.Message);
}
catch (Exception exception)
{
Log.Error(exception, $"Unexpected Exception in {GetType().Name}");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[ApiKey]
[Authorize]
[HttpPut("{documentId:guid}")]
[ProducesResponseType(typeof(DocumentInformationResponse),200)]
[ProducesResponseType(400)]
[ProducesResponseType(401)]
[ProducesResponseType(404)]
[ProducesResponseType(500)]
public async Task<IActionResult> UpdateDocument([FromBody] UpdateDocumentRequest request, Guid documentId)
{
try
{
var result = await _documentService.UpdateDocument(request, documentId, HttpContext.User.Claims.Single(c => c.Type == "name").Value);
if (result == null)
{
return NotFound($"The document with DocumentId:{documentId} could not be found.");
}
return Ok(result);
}
catch (ServiceException ex)
{
return StatusCode(StatusCodes.Status500InternalServerError, ex.Message);
}
catch (Exception exception)
{
Log.Error(exception, $"Unexpected Exception in {GetType().Name}");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[ApiKey]
[Authorize]
[HttpPut("DocumentContent/{documentId:guid}")]
[ProducesResponseType(typeof(DocumentInformationResponse),200)]
[ProducesResponseType(400)]
[ProducesResponseType(401)]
[ProducesResponseType(404)]
[ProducesResponseType(500)]
public async Task<IActionResult> UpdateDocumentContent([FromBody] UpdateDocumentContentRequest request, Guid documentId)
{
try
{
var result = await _documentService.UpdateDocumentContent(request, documentId, HttpContext.User.Claims.Single(c => c.Type == "name").Value);
if (result == null)
{
return NotFound($"The document with DocumentId:{documentId} could not be found.");
}
return Ok(result);
}
catch (ServiceException ex)
{
return StatusCode(StatusCodes.Status500InternalServerError, ex.Message);
}
catch (Exception exception)
{
Log.Error(exception, $"Unexpected Exception in {GetType().Name}");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[ApiKey]
[Authorize]
[HttpPut("DocumentInformation/{documentId:guid}")]
[ProducesResponseType(typeof(DocumentInformationResponse),200)]
[ProducesResponseType(400)]
[ProducesResponseType(401)]
[ProducesResponseType(404)]
[ProducesResponseType(500)]
public async Task<IActionResult> UpdateDocumentInformation([FromBody] UpdateDocumentInformationRequest request,
Guid documentId)
{
try
{
var result = await _documentService.UpdateDocumentInformation(request, documentId, HttpContext.User.Claims.Single(c => c.Type == "name").Value);
if (result == null)
{
return NotFound($"The document with DocumentId:{documentId} could not be found.");
}
return Ok(result);
}
catch (ServiceException ex)
{
return StatusCode(StatusCodes.Status500InternalServerError, ex.Message);
}
catch (Exception exception)
{
Log.Error(exception, $"Unexpected Exception in {GetType().Name}");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[ApiKey]
[HttpGet("DocumentInformation/{documentId:guid}")]
[ProducesResponseType(typeof(DocumentInformationResponse),200)]
[ProducesResponseType(401)]
[ProducesResponseType(404)]
[ProducesResponseType(500)]
public async Task<IActionResult> GetDocumentInformation(Guid documentId)
{
try
{
var result = await _documentService.GetDocumentInformation(documentId);
if (result == null)
{
return NotFound($"The document with DocumentId:{documentId} could not be found.");
}
return Ok(result);
}
catch (ServiceException ex)
{
return StatusCode(StatusCodes.Status500InternalServerError, ex.Message);
}
catch (Exception exception)
{
Log.Error(exception, $"Unexpected Exception in {GetType().Name}");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[ApiKey]
[Authorize]
[HttpPut("DocumentScanStatus")]
[ProducesResponseType(200)]
[ProducesResponseType(400)]
[ProducesResponseType(401)]
[ProducesResponseType(404)]
[ProducesResponseType(500)]
public async Task<IActionResult> UpdateDocumentScanStatus([FromBody] UpdateDocumentScanStatusRequest request)
{
try
{
await _documentService.UpdateDocumentScanStatus(request.ScanStatus, request.DocumentBlobIds, HttpContext.User.Claims.Single(c => c.Type == "name").Value);
return Ok();
}
catch (ServiceException ex)
{
return StatusCode(StatusCodes.Status500InternalServerError, ex.Message);
}
catch (Exception exception)
{
Log.Error(exception, $"Unexpected Exception in {GetType().Name}");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
/// <summary>
/// This is currently a developer endpoint, still waiting on guidance on how the 'Delete' Operation
/// should work. This will remove both the documents contents from blob storage and its information from cosmos.
/// </summary>
/// <param name="documentId"></param>
/// <returns></returns>
[ApiKey]
[HttpDelete("{documentId:guid}")]
[ProducesResponseType(200)]
[ProducesResponseType(204)]
[ProducesResponseType(401)]
[ProducesResponseType(500)]
public async Task<IActionResult> DeleteDocument(Guid documentId)
{
try
{
await _documentService.DeleteDocument(documentId);
return Ok();
}
catch (ServiceException ex)
{
return StatusCode(StatusCodes.Status500InternalServerError, ex.Message);
}
catch (Exception exception)
{
Log.Error(exception, $"Unexpected Exception in {GetType().Name}");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[ApiKey]
[HttpDelete("DeleteDocumentContent")]
[ProducesResponseType(200)]
[ProducesResponseType(204)]
[ProducesResponseType(401)]
[ProducesResponseType(500)]
public async Task<IActionResult> DeleteDocumentContent([FromBody] DeleteDocumentContentRequest request)
{
try
{
await _documentService.DeleteDocumentContent(request);
return Ok();
}
catch (ServiceException ex)
{
return StatusCode(StatusCodes.Status500InternalServerError, ex.Message);
}
catch (Exception exception)
{
Log.Error(exception, $"Unexpected Exception in {GetType().Name}");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[ApiKey]
[HttpPost("Count")]
[ProducesResponseType(typeof(long), 200)]
[ProducesResponseType(401)]
[ProducesResponseType(500)]
public async Task<IActionResult> CountDocuments(DocumentInformationFilter filter)
{
try
{
var count = await _documentService.CountDocuments(filter);
return Ok(count);
}
catch (ServiceException ex)
{
return StatusCode(StatusCodes.Status500InternalServerError);
}
catch (Exception exception)
{
Log.Error(exception, "Unexpected Exception");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
[ApiKey]
[HttpPost("GetDocumentsForCleanup")]
[ProducesResponseType(typeof(IEnumerable<Guid>), 200)]
[ProducesResponseType(204)]
[ProducesResponseType(401)]
[ProducesResponseType(500)]
public async Task<IActionResult> GetDocumentsForCleanup([FromBody] GetDocumentCleanupRequest request)
{
try
{
var documents = await _documentService.GetDocumentsForCleanup(request);
if (!documents.Any())
{
return NoContent();
}
return Ok(documents);
}
catch (ServiceException ex)
{
return StatusCode(StatusCodes.Status500InternalServerError);
}
catch (Exception exception)
{
Log.Error(exception, "Unexpected Exception");
return StatusCode(StatusCodes.Status500InternalServerError);
}
}
}
@@ -0,0 +1,36 @@
using System.Net;
using LCU.API.Internal.Document.Models;
using Microsoft.AspNetCore.Mvc;
using Serilog;
using LCU.Library.Internal.ApiSecurity.Attributes;
namespace LCU.API.Internal.Document.Controllers;
[Route("[controller]")]
[ApiController]
public class StatusController : ControllerBase
{
private readonly IEnvironmentView _environmentView;
public StatusController(IEnvironmentView environmentView)
{
_environmentView = environmentView;
}
[HttpGet]
[ProducesResponseType(200)]
[ProducesResponseType(500)]
public IActionResult Get()
{
try
{
Log.Verbose($"{_environmentView.Application}'s Status Endpoint Was Hit");
return Ok(_environmentView);
}
catch (Exception ex)
{
Log.Error(ex, "Failure to return status");
return StatusCode((int)HttpStatusCode.InternalServerError);
}
}
}
@@ -0,0 +1,12 @@
namespace LCU.API.Internal.Document.Exceptions;
public class BlobApiServiceException : Exception
{
public BlobApiServiceException(string message) : base(message)
{
}
public BlobApiServiceException(string message, Exception exception) : base(message, exception)
{
}
}
@@ -0,0 +1,12 @@
namespace LCU.API.Internal.Document.Exceptions;
public class DocumentAuditServiceException : Exception
{
public DocumentAuditServiceException(string message) : base(message)
{
}
public DocumentAuditServiceException(string message, Exception exception) : base(message, exception)
{
}
}
@@ -0,0 +1,12 @@
namespace LCU.API.Internal.Document.Exceptions;
public class DocumentInformationServiceException : Exception
{
public DocumentInformationServiceException(string message) : base(message)
{
}
public DocumentInformationServiceException(string message, Exception exception) : base(message, exception)
{
}
}
@@ -0,0 +1,41 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<NoWarn>1701;1702; 8618; 1591</NoWarn>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<NoWarn>1701;1702; 8618; 1591</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.Extensions.AspNetCore.Configuration.Secrets" Version="1.2.2" />
<PackageReference Include="Azure.Identity" Version="1.13.2" />
<PackageReference Include="Azure.Security.KeyVault.Secrets" Version="4.3.0" />
<PackageReference Include="LCU.Api.Internal.MaidBotLib" Version="1.0.0" />
<PackageReference Include="LCU.Library.Internal.ApiSecurity" Version="1.0.1" />
<PackageReference Include="LCU.Library.Internal.Exceptions" Version="1.0.0" />
<PackageReference Include="LCU.Library.Internal.Logging" Version="1.1.0" />
<PackageReference Include="Microsoft.FeatureManagement.AspNetCore" Version="2.5.1" />
<PackageReference Include="Microsoft.Identity.Web" Version="1.25.3" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
<PackageReference Include="LCU.Library.Internal.Configuration" Version="1.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\LCU.API.Internal.DocumentLib\LCU.API.Internal.DocumentLib.csproj" />
</ItemGroup>
<ProjectExtensions><VisualStudio><UserProperties appsettings_1staging_1json__JsonSchema="https://github.com/fbecart/zinoma/releases/latest/download/zinoma-schema.json" /></VisualStudio></ProjectExtensions>
<ItemGroup>
<InternalsVisibleTo Include="LCU.Library.Internal.DocumentModelsTest" />
</ItemGroup>
</Project>
@@ -0,0 +1,35 @@
using System.Reflection;
namespace LCU.API.Internal.Document.Models;
public class EnvironmentView : IEnvironmentView
{
private readonly IWebHostEnvironment _env;
public EnvironmentView(IWebHostEnvironment env)
{
_env = env;
}
public string Application => _env.ApplicationName;
public DateTime CurrentDate => DateTime.Now;
public string BuildVersion => AssembleBuildVersion();
public DateTime BuildDate => AssemblyBuildDate();
public string Environment => _env.EnvironmentName;
private static DateTime AssemblyBuildDate()
{
//https://stackoverflow.com/a/804310/266594
var assembly = Assembly.GetExecutingAssembly();
var fileInfo = new FileInfo(assembly.Location);
var lastModified = fileInfo.LastWriteTime;
return lastModified;
}
private static string AssembleBuildVersion()
{
var version = Assembly.GetExecutingAssembly().GetName().Version;
return version == null ? "-.-.-" : version.ToString();
}
}
@@ -0,0 +1,10 @@
namespace LCU.API.Internal.Document.Models;
public interface IEnvironmentView
{
string Application { get; }
DateTime CurrentDate { get; }
string BuildVersion { get; }
DateTime BuildDate { get; }
string Environment { get; }
}
@@ -0,0 +1,263 @@
using System.Text.Json.Serialization;
using Azure.Identity;
using LCU.API.Internal.Document.Configurations;
using LCU.API.Internal.Document.Models;
using LCU.API.Internal.Document.Services;
using LCU.API.Internal.DocumentLib.Configurations;
using LCU.API.Internal.DocumentLib.Containers;
using LCU.API.Internal.DocumentLib.DataAccess;
using LCU.API.Internal.DocumentLib.Repositories;
using LCU.Library.Internal.Logging;
using LCU.Library.Internal.Logging.Middleware;
using LCU.Library.Internal.Logging.Models;
using Microsoft.OpenApi.Models;
using LCU.Library.Internal.ApiSecurity;
using Serilog;
using LCU.API.Internal.DocumentLib.Utilities;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Identity.Web;
using MongoDB.Bson.Serialization;
using LCU.Library.Internal.Configuration;
using Microsoft.FeatureManagement;
var MyAllowSpecificOrigins = "_myAllowSpecificOrigins";
var builder = BuildBuilder(args);
BuildServicesAndConfigurations(builder);
BuildAppAndRun(builder);
WebApplicationBuilder BuildBuilder(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddJsonFile("/app/config/mapped-appsettings/primary.json", true, true);
builder.Configuration.AddJsonFile("/vault/secrets/documentapi_configuration.json", true, true);
builder.Configuration.AddJsonFile("/vault/secrets/document-api-azure-client-id.json", optional: true, reloadOnChange: true);
builder.Configuration.AddJsonFile("/vault/secrets/document-api-azure-tenant-id.json", optional: true, reloadOnChange: true);
builder.Configuration.AddJsonFile("/vault/secrets/document-api-client-id.json", optional: true, reloadOnChange: true);
builder.Configuration.AddJsonFile("/vault/secrets/document-api-client-secret.json", optional: true, reloadOnChange: true);
builder.Configuration.AddJsonFile("/vault/secrets/document-api-tenant-id.json", optional: true, reloadOnChange: true);
builder.Configuration.AddJsonFile("/vault/secrets/document-api-swagger-oauth-scope.json", optional: true, reloadOnChange: true);
builder.Configuration.AddJsonFile("/vault/secrets/document-api-seq-api-key.json", optional: true, reloadOnChange: true);
builder.Configuration.AddJsonFile("/vault/secrets/document-api-blob-storage-connection-string.json", optional: true, reloadOnChange: true);
builder.Configuration.AddJsonFile("/vault/secrets/document-api-mongo-database-connection-string.json", optional: true, reloadOnChange: true);
builder.Configuration.AddJsonFile("/vault/secrets/document-api-type-uploader-api-key.json", optional: true, reloadOnChange: true);
builder.Configuration.AddJsonFile("/vault/secrets/document-api-type-retriever-api-key.json", optional: true, reloadOnChange: true);
builder.Configuration.AddJsonFile("/vault/secrets/document-api-type-cleanup-api-key.json", optional: true, reloadOnChange: true);
if (!bool.Parse(builder.Configuration.GetSection("Primary")["IsAksDeploy"]))
{
var clientSecretCredentials = new ClientSecretCredential(builder.Configuration["TenantId"], builder.Configuration["ClientId"], builder.Configuration["ClientSecret"]);
builder.Configuration.AddAzureKeyVault(
new Uri($"https://{builder.Configuration["AzureKeyVaultUrl"]}.vault.azure.net/"), clientSecretCredentials);
}
builder.Host.UseSerilog();
return builder;
}
void BuildServicesAndConfigurations(WebApplicationBuilder builder)
{
var services = builder.Services;
var configuration = builder.Configuration;
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(configuration.GetSection("AzureAd"));
services.AddAuthorization(options =>
{
options.AddPolicy("user_token", policy =>
{
policy.AuthenticationSchemes.Add(JwtBearerDefaults.AuthenticationScheme);
policy.RequireAuthenticatedUser();
});
options.DefaultPolicy = options.GetPolicy("user_token")!;
});
services.AddFeatureManagement();
var documentTypeConfiguration = configuration.Bind<ApiConfiguration>(services, "Primary");
configuration.Bind<BlobStorageConfiguration>(services, "BlobStorage");
configuration.Bind<DatabaseConfiguration>(services, "MongoDatabase");
configuration.Bind<NotificationApiConfiguration>(services);
var loggerConfiguration = new LoggingConfiguration(documentTypeConfiguration.AppName,
documentTypeConfiguration.SeqApiKey,
documentTypeConfiguration.SeqUrl);
var lcuLogging = new LcuLogging(loggerConfiguration);
lcuLogging.UseLcuLogging();
ServiceRegistrations.AddDocumentServices(services);
ServiceRegistrations.AddDocumentDataAccesses(services);
ServiceRegistrations.AddDocumentRepositories(services);
BsonSerializer.RegisterDiscriminatorConvention(typeof(DocumentAudit), new DocumentAuditDiscriminatorConvention());
services.AddControllers().AddJsonOptions(options => options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()));
services.AddEndpointsApiExplorer();
services.AddSwaggerGen(s =>
{
s.SwaggerDoc("v1", new OpenApiInfo { Title = $"{documentTypeConfiguration.AppName}", Version = "v1" });
s.UseOneOfForPolymorphism();
var authority = $"https://login.microsoftonline.com/{configuration.GetSection("AzureAd")["TenantId"]}";
s.AddSecurityDefinition
(
nameof(SecuritySchemeType.OpenIdConnect),
new OpenApiSecurityScheme
{
Type = SecuritySchemeType.OAuth2,
Flows = new OpenApiOAuthFlows
{
AuthorizationCode = new OpenApiOAuthFlow
{
AuthorizationUrl = new Uri($"{authority}/oauth2/v2.0/authorize"),
TokenUrl = new Uri($"{authority}/oauth2/v2.0/token")
}
}
//24.10.2022 these SHOULD have enabled OIDC discovery (the discovery info is returned from this URI) but swagger UI is not displaying the auth options
//oidc discovery does work, I have set it up with IdentityServer4 in the past
//Type = SecuritySchemeType.OpenIdConnect,
//OpenIdConnectUrl = new Uri($"https://login.microsoftonline.com/{configuration.GetSection("AzureAd")["TenantId"]}/v2.0/.well-known/openid-configuration")
}
);
s.AddSecurityRequirement
(
new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = nameof(SecuritySchemeType.OpenIdConnect)
}
},
Array.Empty<string>()
}
}
);
s.AddSecurityDefinition("ApiKey", new OpenApiSecurityScheme
{
In = ParameterLocation.Header,
Name = "ApiKey"
});
s.AddSecurityDefinition("ApiClient", new OpenApiSecurityScheme
{
In = ParameterLocation.Header,
Name = "Client"
});
s.AddSecurityRequirement
(
new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "ApiKey"
}
},
Array.Empty<string>()
}
}
);
s.AddSecurityRequirement
(
new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "ApiClient"
}
},
Array.Empty<string>()
}
}
);
});
services.AddApiKeySecurity(configuration);
}
void BuildAppAndRun(WebApplicationBuilder builder)
{
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment()) app.UseDeveloperExceptionPage();
app.UseSwagger(s =>
{
s.PreSerializeFilters.Add((doc, request) =>
{
doc.Servers = new List<OpenApiServer> { new OpenApiServer { Url = $"{request.Scheme}://{request.Host.Value}{request.PathBase}" } };
});
});
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("v1/swagger.json", "LCU.API.Internal.Document v1");
c.OAuthClientId(app.Configuration.GetSection("AzureAd")["ClientId"]);
c.OAuthUsePkce();
c.OAuthScopes(app.Configuration.GetSection("Swagger")["OAuthScope"]);
});
app.UseMiddleware<RequestResponseLoggingMiddleware>();
app.UseSerilogRequestLogging(options =>
{
options.EnrichDiagnosticContext = RequestResponseLoggingMiddleware.RequestDataParser.EnrichFromRequest;
});
app.UseRouting();
app.UseCors(MyAllowSpecificOrigins);
app.UseHttpsRedirection();
app.UseAuthentication().UseAuthorization();
app.MapControllers();
app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
app.Run();
}
public static class ServiceRegistrations
{
public static void AddDocumentServices(IServiceCollection services)
{
services.AddScoped<DocumentService.DocumentIsForMemberStrategy>(p =>
{
const string checkDocumentOwner = "GetDocumentCheckOwnership";
return p.GetRequiredService<IFeatureManager>().IsEnabledAsync(checkDocumentOwner).Result
? DocumentService.Ownership.CheckPersonOrOrgNumber.Strategy
: DocumentService.Ownership.Unchecked.Strategy;
});
services.AddScoped<IDocumentService, DocumentService>();
services.AddScoped<IDocumentInformationService, DocumentInformationService>();
services.AddScoped<IBlobApiService, BlobApiService>();
services.AddScoped<IDocumentAuditService, DocumentAuditService>();
services.AddScoped<IEnvironmentView, EnvironmentView>();
services.AddScoped<INotificationApiService, NotificationApiService>();
}
public static void AddDocumentDataAccesses(IServiceCollection services)
{
services.AddScoped<IBlobStorageClient, BlobStorageClient>();
services.AddScoped<IBlobApiDao, BlobApiDao>();
}
public static void AddDocumentRepositories(IServiceCollection services)
{
services.AddTransient<IDataClient, DataClient>();
services.AddTransient<IMongoCollectionFactory, MongoCollectionFactory>();
services.AddScoped<IDocumentInformationRepository, DocumentInformationRepository>();
services.AddScoped<IDocumentAuditRepository, DocumentAuditRepository>();
services.AddScoped<INotificationApiDao, NotificationApiDao>();
}
}
@@ -0,0 +1,31 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:9009",
"sslPort": 44312
}
},
"profiles": {
"LCU.API.Internal.Document": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:7037;http://localhost:5037",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
@@ -0,0 +1,96 @@
using LCU.API.Internal.Document.Exceptions;
using LCU.API.Internal.DocumentLib.DataAccess;
using LCU.Library.Internal.Exceptions;
using Serilog;
namespace LCU.API.Internal.Document.Services;
public class BlobApiService : IBlobApiService
{
private readonly IBlobApiDao _blobApiDao;
public BlobApiService(IBlobApiDao blobApiDao)
{
_blobApiDao = blobApiDao;
}
public async Task<byte[]> GetDocumentContent(Guid? documentBlobId)
{
try
{
if (documentBlobId is null || documentBlobId == Guid.Empty)
{
return Array.Empty<byte>();
}
var result = await _blobApiDao.GetDocumentFromBlob(documentBlobId ?? Guid.Empty);
if (result?.Value == null){
throw new ServiceException($"Could not find the Document Content for DocumentBlobId:{documentBlobId}");
}
await using var memoryStream = new MemoryStream();
await result.Value.Content.CopyToAsync(memoryStream);
return memoryStream.ToArray();
}
catch (ServiceException exception){
Log.Error(exception.Message);
throw new BlobApiServiceException(exception.Message);
}
catch (Exception exception){
var message = $"Failure to Retrieve Document Content with DocumentBlobId: {documentBlobId}.";
Log.Error(exception, message);
throw new BlobApiServiceException($"{message} Exception Message: {exception.Message}");
}
}
public async Task<Guid> UploadDocumentContent(byte[] content)
{
try
{
return await _blobApiDao.SaveDocumentToBlob(content);
}
catch (Exception exception){
const string message = "Failure to Upload Document Content.";
Log.Error(exception, message);
throw new BlobApiServiceException($"{message}. Exception Message: {exception.Message}");
}
}
public async Task UpdateDocumentContent(Guid? documentBlobId, byte[] content)
{
try
{
if (documentBlobId is null || documentBlobId == Guid.Empty)
{
throw new Exception("DocumentBlobId cannot be null.");
}
await _blobApiDao.UpdateDocumentInBlob(documentBlobId ?? Guid.Empty, content);
}
catch (Exception exception){
var message = $"Failure to Update Document with DocumentBlobId: {documentBlobId}";
Log.Error(exception, message);
throw new BlobApiServiceException($"{message} Exception Message: {exception.Message}");
}
}
public async Task DeleteDocumentContent(Guid? documentBlobId)
{
try
{
if (documentBlobId is null || documentBlobId == Guid.Empty)
{
throw new Exception("DocumentBlobId cannot be null.");
}
await _blobApiDao.RemoveDocumentFromBlob(documentBlobId ?? Guid.Empty);
}
catch (Exception exception){
var message = $"Failure to Delete Document with DocumentBlobId: {documentBlobId}";
Log.Error(exception, message);
throw new BlobApiServiceException($"{message} Exception Message: {exception.Message}");
}
}
}
@@ -0,0 +1,243 @@
using LCU.API.Internal.Document.Exceptions;
using LCU.API.Internal.DocumentLib.Containers;
using LCU.API.Internal.DocumentLib.Repositories;
using LCU.Library.Internal.DocumentModels.Requests;
using LCU.Library.Internal.DocumentModels.Responses;
using Serilog;
namespace LCU.API.Internal.Document.Services;
public class DocumentAuditService : IDocumentAuditService
{
private const string NameField = "Name";
private const string TypeField = "Type";
private const string AttributesField = "Attributes";
private const string ScanStatusField = "ScanStatus";
private const string ContentField = "Content";
private readonly IDocumentAuditRepository _documentAuditRepository;
public DocumentAuditService(IDocumentAuditRepository documentAuditRepository)
{
_documentAuditRepository = documentAuditRepository;
}
public async Task<List<DocumentAuditResponse>> GetDocumentAudits(params Guid[] documentIds)
{
try
{
static DocumentAuditResponse ResponseFactory(DocumentAudit audit) => audit switch
{
DocumentActionAudit actionAudit => new DocumentActionAuditResponse
{
Action = actionAudit.Action,
ActionDescription = actionAudit.ActionDescription,
Date = actionAudit.Date,
DocumentId = actionAudit.DocumentId,
Source = actionAudit.Source,
UserId = actionAudit.UserId
},
DocumentFieldAudit fieldAudit => new DocumentFieldAuditResponse
{
Field = fieldAudit.Field,
CurrentValue = fieldAudit.CurrentValue,
PreviousValue = fieldAudit.PreviousValue,
Date = fieldAudit.Date,
DocumentId = fieldAudit.DocumentId,
Source = fieldAudit.Source,
UserId = fieldAudit.UserId
},
_ => throw new ArgumentOutOfRangeException(nameof(audit))
};
return (await _documentAuditRepository.GetDocumentAudits(documentIds))
.Select(ResponseFactory)
.ToList();
}
catch (Exception exception)
{
var message = $"Failure to Retrieve Document Audit Information for DocumentIds: {documentIds}";
Log.Error(exception, message);
throw new DocumentAuditServiceException($"{message} Exception Message: {exception.Message}");
}
}
public async Task<IEnumerable<DocumentAudit>> CreateDocumentUploadedAudit(Guid documentId, DateTime uploadDate)
{
try
{
var audits = new DocumentAudit[]
{
new DocumentActionAudit
(
documentId,
"System",
uploadDate,
"System",
DocumentAction.Upload,
"Document Uploaded"
)
};
return await _documentAuditRepository.SaveDocumentAudits(audits);
}
catch (Exception exception)
{
var message = $"Failure to Create Document Audit for DocumentId: {documentId}";
Log.Error(exception, message);
throw new DocumentAuditServiceException($"{message} Exception Message: {exception.Message}");
}
}
public async Task<IEnumerable<DocumentAudit>> CreateDocumentDeletedAudit(Guid documentId,
DateTime deleteDate)
{
try
{
var audits = new DocumentAudit[]
{
new DocumentActionAudit
(
documentId,
"System",
deleteDate,
"System",
DocumentAction.Delete,
"Document Deleted"
)
};
return await _documentAuditRepository.SaveDocumentAudits(audits);
}
catch (Exception exception)
{
var message = $"Failure to Create Document Audit for DocumentId: {documentId}";
Log.Error(exception, message);
throw new DocumentAuditServiceException($"{message} Exception Message: {exception.Message}");
}
}
public async Task<IEnumerable<DocumentAudit>> CreateDocumentContentDeletedAudit(Guid documentId,
DateTime deleteDate,
string? reason)
{
try
{
var audits = new DocumentAudit[]
{
new DocumentActionAudit
(
documentId,
"System",
deleteDate,
"System",
DocumentAction.DeleteContent,
"DocumentContent Deleted" + (!string.IsNullOrWhiteSpace(reason) ? $". Reason: {reason}" : string.Empty)
)
};
return await _documentAuditRepository.SaveDocumentAudits(audits);
}
catch (Exception exception)
{
var message = $"Failure to Create Document Audit for DocumentId: {documentId}";
Log.Error(exception, message);
throw new DocumentAuditServiceException($"{message} Exception Message: {exception.Message}");
}
}
public async Task<IEnumerable<DocumentAudit>> CreateDocumentAudits(DocumentInformation oldDocumentInformation,
DocumentInformation newDocumentInformation, bool wasDocumentContentUpdated, DateTime updateDate, string userId)
{
DocumentFieldAudit AuditLogFactory(string field, object? oldValue, object? newValue) => new
(
newDocumentInformation.DocumentId,
newDocumentInformation.Source,
updateDate,
userId,
field,
oldValue,
newValue
);
var documentAudits = new List<DocumentFieldAudit>();
try
{
if (oldDocumentInformation.Name != newDocumentInformation.Name){
documentAudits.Add(AuditLogFactory(NameField, oldDocumentInformation.Name, newDocumentInformation.Name));
}
if (oldDocumentInformation.Type != newDocumentInformation.Type){
documentAudits.Add(AuditLogFactory(TypeField, oldDocumentInformation.Type, newDocumentInformation.Type));
}
if (oldDocumentInformation.ScanStatus != newDocumentInformation.ScanStatus){
documentAudits.Add(AuditLogFactory(ScanStatusField, oldDocumentInformation.ScanStatus, newDocumentInformation.ScanStatus));
}
if (!AttributesAreEqual(oldDocumentInformation.Attributes, newDocumentInformation.Attributes)){
documentAudits.Add(AuditLogFactory(AttributesField, oldDocumentInformation.Attributes, newDocumentInformation.Attributes));
}
if (wasDocumentContentUpdated){
documentAudits.Add(AuditLogFactory(ContentField, "NOT CAPTURED", "NOT CAPTURED"));
}
if (!documentAudits.Any()){
return new List<DocumentAudit>();
}
return await _documentAuditRepository.SaveDocumentAudits(documentAudits.ToArray());
}
catch (Exception exception){
var message =
$"Failure to Create Document Audit Information for DocumentId: {oldDocumentInformation.DocumentId}";
Log.Error(exception, message);
throw new DocumentAuditServiceException($"{message}. Exception Message: {exception.Message}");
}
}
public async Task AuditAction(Guid documentId, string userId, string actionSource, DocumentAction action)
{
try
{
var description = action switch
{
DocumentAction.Download => $"Document downloaded from {actionSource}.",
DocumentAction.Upload => "Document Uploaded",
DocumentAction.Delete => "Document Deleted",
DocumentAction.DeleteContent => "DocumentContent Deleted",
_ => throw new ArgumentOutOfRangeException()
};
await _documentAuditRepository.SaveDocumentAudits(new DocumentActionAudit(documentId, actionSource, DateTime.Now, userId, action, description));
}
catch (Exception exception)
{
var message =
$"Failure to Create Document Audit Information for DocumentId: {documentId} on DocumentAction: {action} from source {actionSource}";
Log.Error(exception, message);
throw new DocumentAuditServiceException($"{message}. Exception Message: {exception.Message}");
}
}
private static bool AttributesAreEqual(Dictionary<string, string>? oldAttributes, Dictionary<string, string>? newAttributes)
{
oldAttributes ??= new Dictionary<string, string>();
newAttributes ??= new Dictionary<string, string>();
if (oldAttributes.Count != newAttributes.Count){
return false;
}
var attributes = oldAttributes
.Where(d2 => newAttributes.Any(d1 => d1.Key == d2.Key && d2.Value.Equals(d1.Value))).ToList();
return attributes.Count == oldAttributes.Count;
}
}
@@ -0,0 +1,164 @@
using LCU.API.Internal.Document.Configurations;
using LCU.API.Internal.Document.Exceptions;
using LCU.API.Internal.DocumentLib.Containers;
using LCU.API.Internal.DocumentLib.Repositories;
using LCU.Library.Internal.DocumentIngestorModels.Models.Pagination;
using LCU.Library.Internal.DocumentModels.Requests.Search;
using Serilog;
namespace LCU.API.Internal.Document.Services;
public class DocumentInformationService : IDocumentInformationService
{
private readonly IDocumentInformationRepository _documentInformationRepository;
private readonly ApiConfiguration _apiConfiguration;
public DocumentInformationService(IDocumentInformationRepository documentInformationRepository,
ApiConfiguration apiConfiguration)
{
_documentInformationRepository = documentInformationRepository;
_apiConfiguration = apiConfiguration;
}
public async Task<DocumentInformation?> GetDocumentInformation(Guid documentId)
{
try
{
var documentInformation = await _documentInformationRepository.GetDocumentInformation(documentId);
if (documentInformation == null ||
documentInformation.UploadDate >= DateTime.UtcNow.AddHours(_apiConfiguration.FileUploadCutoffHours) ||
documentInformation.ScanStatus == false)
{
return null;
}
return documentInformation;
}
catch (Exception exception)
{
var message = $"Failure to Retrieve DocumentInformation with DocumentId: {documentId}.";
Log.Error(exception, message);
throw new DocumentInformationServiceException($"{message} Exception Message: {exception.Message}");
}
}
public async Task<DocumentInformation?> GetDocumentInformationByBlobId(Guid? documentBlobId)
{
try
{
if (documentBlobId is null || documentBlobId == Guid.Empty)
{
return null;
}
var documentInformation = await _documentInformationRepository.GetDocumentInformationByBlobId(documentBlobId ?? Guid.Empty);
return documentInformation ?? null;
}
catch (Exception exception)
{
var message = $"Failure to Retrieve DocumentInformation with DocumentBlobId: {documentBlobId}.";
Log.Error(exception, message);
throw new DocumentInformationServiceException($"{message} Exception Message: {exception.Message}");
}
}
public async Task DeleteDocumentInformation(Guid documentId)
{
try
{
var documentInformation = await _documentInformationRepository.GetDocumentInformation(documentId);
if (documentInformation == null)
{
return;
}
await _documentInformationRepository.RemoveDocumentInformation(documentId);
}
catch (Exception exception)
{
var message = $"Failure to Delete DocumentInformation with DocumentId: {documentId}";
Log.Error(exception, message);
throw new DocumentInformationServiceException($"{message} Exception Message: {exception.Message}");
}
}
public async Task<DocumentInformation> UploadDocumentInformation(DocumentInformation information)
{
try{
return await _documentInformationRepository.SaveDocumentInformation(information);
}
catch (Exception exception)
{
var message = $"Failure to Upload DocumentInformation with Name: {information.Name} of type: {information.Type} from source: {information.Source}";
Log.Error(exception, message);
throw new DocumentInformationServiceException($"{message} Exception Message: {exception.Message}");
}
}
public async Task<DocumentInformation?> UpdateDocumentInformation(Guid documentId, DocumentInformation information)
{
try
{
var documentInformation = await _documentInformationRepository.GetDocumentInformation(documentId);
if (documentInformation == null)
{
return null;
}
return await _documentInformationRepository.UpdateDocumentInformation(documentId, information);
}
catch (Exception exception)
{
var message = $"Failure to Update DocumentInformation with DocumentId: {documentId} and Name: {information.Name} of type: {information.Type}.";
Log.Error(exception, message);
throw new DocumentInformationServiceException($"{message} Exception Message: {exception.Message}");
}
}
public async Task<List<DocumentInformation>> SearchDocumentInformation(DocumentInformationFilter filter, int? skip = null, int? take = null, List<SortableField>? sortDefinition = null)
{
try
{
var documentInformationFound = await _documentInformationRepository.SearchDocumentInformation(filter, skip, take, sortDefinition);
return documentInformationFound ?? new List<DocumentInformation>();
}
catch (Exception exception)
{
const string message = "Failure to Search Document Information";
Log.Error(exception, message);
throw new DocumentInformationServiceException($"{message} Exception Message: {exception.Message}");
}
}
public async Task<long> CountDocuments(DocumentInformationFilter filter)
{
try
{
return await _documentInformationRepository.CountDocuments(filter);
}
catch (Exception exception)
{
const string message = "Failure to Count Document Information";
Log.Error(exception, message);
throw new DocumentInformationServiceException($"{message} Exception Message: {exception.Message}");
}
}
public async Task<List<Guid>> GetDocumentsForCleanup(GetDocumentCleanupRequest request)
{
try
{
return await _documentInformationRepository.GetDocumentsForCleanup(request);
}
catch (Exception exception)
{
const string message = "Failure to GetDocumentsForCleanup";
Log.Error(exception, message);
throw new DocumentInformationServiceException($"{message} Exception Message: {exception.Message}");
}
}
}
@@ -0,0 +1,748 @@
using System.Numerics;
using System.Text.Json;
using LCU.API.Internal.Document.Exceptions;
using LCU.API.Internal.DocumentLib.Containers;
using LCU.Library.Internal.DocumentModels.Requests;
using LCU.Library.Internal.DocumentModels.Requests.Search;
using LCU.Library.Internal.DocumentModels.Responses;
using LCU.Library.Internal.Exceptions;
using LCU.Api.Internal.MaidBotLib.Models;
using Serilog;
using LCU.API.Internal.NotificationsApiLib.Models;
using LCU.API.Internal.Document.Configurations;
using LCU.Library.Internal.DocumentIngestorModels.Models.Pagination;
namespace LCU.API.Internal.Document.Services;
// TODO: Add hash of content to document information so the content can be compared before updates.
// Any basic hash should be fine since it will only be used as a checksum to the content.
// Example can be found https://stackoverflow.com/questions/10520048/calculate-md5-checksum-for-a-file/10520086#10520086
public class DocumentService : IDocumentService
{
private readonly IBlobApiService _blobApiService;
private readonly IDocumentInformationService _documentInformationService;
private readonly IDocumentAuditService _documentAuditService;
private readonly INotificationApiService _notificationApiService;
private readonly ApiConfiguration _apiConfiguration;
private readonly DocumentIsForMemberStrategy DocumentIsForMember;
public DocumentService
(
IBlobApiService blobApiService,
IDocumentInformationService documentInformationService,
IDocumentAuditService documentAuditService,
INotificationApiService notificationApiService,
ApiConfiguration apiConfiguration,
DocumentIsForMemberStrategy documentIsForMember
)
{
_blobApiService = blobApiService;
_documentInformationService = documentInformationService;
_documentAuditService = documentAuditService;
_notificationApiService = notificationApiService;
_apiConfiguration = apiConfiguration;
DocumentIsForMember = documentIsForMember;
}
public async Task<DocumentResponse?> GetDocument(Guid documentId)
{
try{
var documentInformation = await _documentInformationService.GetDocumentInformation(documentId);
if (documentInformation == null)
{
return null;
}
return new DocumentResponse
{
Content = await _blobApiService.GetDocumentContent(documentInformation.DocumentBlobId),
DocumentInformation = ConvertDocumentInformationToDocumentInformationResponse(documentInformation)
};
}
catch (DocumentInformationServiceException exception){
var message = $"Failure to Retrieve Document Information. Exception Message: {exception.Message}";
Log.Error(exception, message);
throw new ServiceException(message);
}
catch (BlobApiServiceException exception){
var message =
$"Failure to Retrieve Document Content from Blob Storage. Exception Message: {exception.Message}";
Log.Error(exception, message);
throw new ServiceException(message);
}
catch (Exception exception){
var message = $"Failure to Retrieve Document with DocumentId: {documentId}.";
Log.Error(exception, message);
throw new ServiceException($"{message} Exception Message: {exception.Message}");
}
}
public async Task<DocumentResponse?> GetDocument(long personOrOrgNumber, Guid documentId)
{
try{
var documentInformation = await _documentInformationService.GetDocumentInformation(documentId);
if (documentInformation is null)
{
return null;
}
if(!DocumentIsForMember(documentInformation, personOrOrgNumber)) return null;
return new DocumentResponse
{
Content = await _blobApiService.GetDocumentContent(documentInformation.DocumentBlobId),
DocumentInformation = ConvertDocumentInformationToDocumentInformationResponse(documentInformation)
};
}
catch (DocumentInformationServiceException exception){
var message = $"Failure to Retrieve Document Information. Exception Message: {exception.Message}";
Log.Error(exception, message);
throw new ServiceException(message);
}
catch (BlobApiServiceException exception){
var message =
$"Failure to Retrieve Document Content from Blob Storage. Exception Message: {exception.Message}";
Log.Error(exception, message);
throw new ServiceException(message);
}
catch (Exception exception){
var message = $"Failure to Retrieve Document with DocumentId: {documentId}.";
Log.Error(exception, message);
throw new ServiceException($"{message} Exception Message: {exception.Message}");
}
}
public async Task<byte[]?> GetDocumentByBlobId(Guid documentBlobId)
{
try
{
return await _blobApiService.GetDocumentContent(documentBlobId);
}
catch (BlobApiServiceException exception)
{
var message =
$"Failure to Retrieve Document Content from Blob Storage. Exception Message: {exception.Message}";
Log.Error(exception, message);
throw new ServiceException(message);
}
catch (Exception exception)
{
var message = $"Failure to Retrieve Document with DocumentBlobId: {documentBlobId}.";
Log.Error(exception, message);
throw new ServiceException($"{message} Exception Message: {exception.Message}");
}
}
public async Task<byte[]?> GetDocumentByBlobId(long personOrOrgNumber, Guid documentBlobId)
{
try
{
var document = await _documentInformationService.GetDocumentInformationByBlobId(documentBlobId);
if(document is null) return null;
if(!DocumentIsForMember(document, personOrOrgNumber)) return null;
return await _blobApiService.GetDocumentContent(documentBlobId);
}
catch (BlobApiServiceException exception)
{
var message =
$"Failure to Retrieve Document Content from Blob Storage. Exception Message: {exception.Message}";
Log.Error(exception, message);
throw new ServiceException(message);
}
catch (Exception exception)
{
var message = $"Failure to Retrieve Document with DocumentBlobId: {documentBlobId}.";
Log.Error(exception, message);
throw new ServiceException($"{message} Exception Message: {exception.Message}");
}
}
public async Task DeleteDocument(Guid documentId)
{
try{
var deleteDate = DateTime.UtcNow;
var documentInformation = await _documentInformationService.GetDocumentInformation(documentId);
if (documentInformation == null)
{
return;
}
await _documentInformationService.DeleteDocumentInformation(documentId);
await _blobApiService.DeleteDocumentContent(documentInformation.DocumentBlobId);
await _documentAuditService.CreateDocumentDeletedAudit(documentInformation.DocumentId, deleteDate);
}
catch (DocumentInformationServiceException exception){
var message = $"Failure to Remove Document Information. Exception Message: {exception.Message}";
Log.Error(exception, message);
throw new ServiceException(message);
}
catch (BlobApiServiceException exception){
var message =
$"Failure to Remove Document Content from Blob Storage, but DocumentInformation was removed. Exception Message: {exception.Message}";
Log.ForContext(Enum.GetName(typeof(PropertyEnum), PropertyEnum.Group),
Enum.GetName(typeof(GroupEnum), GroupEnum.ITDevelopment)).Error(exception, message);
throw new ServiceException(message);
}
catch (Exception exception){
var message = $"Failure to Delete Document with DocumentId: {documentId}";
Log.Error(exception, message);
throw new ServiceException($"{message} Exception Message: {exception.Message}");
}
}
public async Task DeleteDocumentContent(DeleteDocumentContentRequest request)
{
try
{
var deleteDate = DateTime.UtcNow;
var documentInformation = await _documentInformationService.GetDocumentInformation(request.DocumentId);
if (documentInformation?.DocumentBlobId is null)
{
return;
}
await _blobApiService.DeleteDocumentContent(documentInformation.DocumentBlobId);
documentInformation.DocumentBlobId = null;
await _documentInformationService.UpdateDocumentInformation(documentInformation.DocumentId, documentInformation);
await _documentAuditService.CreateDocumentContentDeletedAudit(documentInformation.DocumentId, deleteDate, request.Reason);
}
catch (DocumentInformationServiceException exception){
var message =
$"Failure to Remove Document Content from Blob Storage. Exception Message: {exception.Message}";
Log.Error(exception, message);
throw new ServiceException(message);
}
catch (BlobApiServiceException exception){
var message =
$"Failure to Remove Document Content from Blob Storage. Exception Message: {exception.Message}";
Log.ForContext(Enum.GetName(typeof(PropertyEnum), PropertyEnum.Group),
Enum.GetName(typeof(GroupEnum), GroupEnum.ITDevelopment)).Error(exception, message);
throw new ServiceException(message);
}
catch (Exception exception){
var message = $"Failure to Delete Document Content with DocumentId: {request.DocumentId}";
Log.Error(exception, message);
throw new ServiceException($"{message} Exception Message: {exception.Message}");
}
}
public async Task<DocumentInformationResponse> UploadDocument(DocumentRequest request)
{
var docBlobId = Guid.Empty;
try{
docBlobId = await _blobApiService.UploadDocumentContent(request.Content);
var fileSize = CalculateFileSize(request.Content);
var uploadDate = DateTime.UtcNow;
var documentInformation = new DocumentInformation
{
DocumentId = Guid.NewGuid(),
DocumentBlobId = docBlobId,
TraceId = request.TraceId,
Name = request.Name,
Type = request.Type,
Source = request.Source,
DataSource = request.DataSource,
PersonNumbers = request.PersonNumbers,
OrganizationNumbers = request.OrganizationNumbers,
AccountNumber = request.AccountNumber,
Attributes = request.Attributes,
CreatedDate = request.CreatedDate,
FileSize = fileSize,
UploadDate = DateTime.UtcNow,
ScanStatus = null
};
var uploadedDocumentInformation = await _documentInformationService.UploadDocumentInformation(documentInformation);
await _documentAuditService.CreateDocumentUploadedAudit(documentInformation.DocumentId, uploadDate);
return ConvertDocumentInformationToDocumentInformationResponse(uploadedDocumentInformation);
}
catch (BlobApiServiceException exception){
var message = $"Failure to Upload Content to Blob Storage. Exception Message: {exception.Message}";
Log.Error(exception, message);
throw new ServiceException(message);
}
catch (DocumentInformationServiceException exception){
await RollBackDocumentContent(docBlobId);
var message = $"Failure to Upload Document Information. Exception Message: {exception.Message}";
Log.Error(exception, message);
throw new ServiceException(message);
}
catch (Exception exception){
var message =
$"Failure to Upload Document Name: {request.Name} of type: {request.Type} from source: {request.Source}";
Log.Error(exception, message);
throw new ServiceException($"{message}. Exception Message: {exception.Message}");
}
}
public async Task<DocumentInformationResponse?> UpdateDocument(UpdateDocumentRequest request, Guid documentId, string userId)
{
DocumentInformation? documentInformation = null;
var modifiedDate = DateTime.UtcNow;
try{
documentInformation = await _documentInformationService.GetDocumentInformation(documentId);
if (documentInformation == null)
{
return null;
}
var content = request.Content;
var fileSize = CalculateFileSize(content);
var updatedDocumentInformation = new DocumentInformation
{
DocumentId = documentId,
DocumentBlobId = documentInformation.DocumentBlobId,
TraceId = documentInformation.TraceId,
UploadDate = documentInformation.UploadDate,
Source = documentInformation.Source,
DataSource = documentInformation.DataSource,
CreatedDate = documentInformation.CreatedDate,
Name = request.Name,
Type = request.Type,
PersonNumbers = request.PersonNumbers,
OrganizationNumbers = request.OrganizationNumbers,
AccountNumber = request.AccountNumber,
Attributes = request.Attributes,
LastModifiedDate = modifiedDate,
FileSize = fileSize,
ScanStatus = null
};
var docInfo = await _documentInformationService.UpdateDocumentInformation(documentId, updatedDocumentInformation);
await _documentAuditService.CreateDocumentAudits(documentInformation, updatedDocumentInformation, true, modifiedDate, userId);
await _blobApiService.UpdateDocumentContent(documentInformation.DocumentBlobId, content);
return ConvertDocumentInformationToDocumentInformationResponse(docInfo);
}
catch (DocumentInformationServiceException exception){
var message = $"Failure to Update Document Information for DocumentId: {documentId}. Exception Message: {exception.Message}";
Log.Error(exception, message);
throw new ServiceException(message);
}
catch (DocumentAuditServiceException exception){
await RollbackDocumentInformation(documentId, documentInformation);
var message = $"Failure to Create Document Audits. Exception Message: {exception.Message}";
Log.Error(exception, message);
throw new ServiceException(message);
}
catch (BlobApiServiceException exception){
await RollbackDocumentInformation(documentId, documentInformation);
var message = $"Failure to Update Document Content from Blob Storage. Exception Message: {exception.Message}";
Log.Error(exception, message);
throw new ServiceException(message);
}
catch (Exception exception){
var message = $"Failure to Update Document with DocumentId: {documentId} Name: {request.Name} of type: {request.Type} from source: {request.UpdateSource}.";
Log.Error(exception, message);
throw new ServiceException($"{message}. Exception Message: {exception.Message}");
}
}
public async Task<DocumentInformationResponse?> GetDocumentInformation(Guid documentId)
{
try{
var documentInformation = await _documentInformationService.GetDocumentInformation(documentId);
if (documentInformation == null)
{
return null;
}
return ConvertDocumentInformationToDocumentInformationResponse(documentInformation);
}
catch (DocumentInformationServiceException exception){
var message = $"Failure to Retrieve Document Information. Exception Message: {exception.Message}";
Log.Error(exception, message);
throw new ServiceException(message);
}
catch (Exception exception){
var message = $"Failure to Retrieve Document with DocumentId: {documentId}.";
Log.Error(exception, message);
throw new ServiceException($"{message} Exception Message: {exception.Message}");
}
}
public async Task<DocumentInformationResponse?> UpdateDocumentContent(UpdateDocumentContentRequest request, Guid documentId, string userId)
{
DocumentInformation? documentInformation = null;
var modifiedDate = DateTime.UtcNow;
try{
documentInformation = await _documentInformationService.GetDocumentInformation(documentId);
if (documentInformation == null)
{
return null;
}
var content = request.Content;
var fileSize = CalculateFileSize(content);
var updatedDocumentInformation = new DocumentInformation
{
DocumentId = documentId,
DocumentBlobId = documentInformation.DocumentBlobId,
TraceId = documentInformation.TraceId,
Name = documentInformation.Name,
Type = documentInformation.Type,
Source = documentInformation.Source,
DataSource = documentInformation.DataSource,
PersonNumbers = documentInformation.PersonNumbers,
OrganizationNumbers = documentInformation.OrganizationNumbers,
AccountNumber = documentInformation.AccountNumber,
Attributes = documentInformation.Attributes,
UploadDate = documentInformation.UploadDate,
CreatedDate = documentInformation.CreatedDate,
LastModifiedDate = modifiedDate,
FileSize = fileSize,
ScanStatus = null
};
var docInfo = await _documentInformationService.UpdateDocumentInformation(documentId, updatedDocumentInformation);
await _documentAuditService.CreateDocumentAudits(documentInformation, updatedDocumentInformation, true, modifiedDate, userId);
await _blobApiService.UpdateDocumentContent(documentInformation.DocumentBlobId, content);
return ConvertDocumentInformationToDocumentInformationResponse(docInfo);
}
catch (DocumentInformationServiceException exception){
var message = $"Failure to Update Document Information for DocumentId: {documentId}. Exception Message: {exception.Message}";
Log.Error(exception, message);
throw new ServiceException(message);
}
catch (DocumentAuditServiceException exception){
await RollbackDocumentInformation(documentId, documentInformation);
var message = $"Failure to Create Document Audits. Exception Message: {exception.Message}";
Log.Error(exception, message);
throw new ServiceException(message);
}
catch (BlobApiServiceException exception){
await RollbackDocumentInformation(documentId, documentInformation);
var message = $"Failure to Update Document Content from Blob Storage. Exception Message: {exception.Message}";
Log.Error(exception, message);
throw new ServiceException(message);
}
catch (Exception exception){
var message = $"Failure to Update Document Content for DocumentId: {documentId}.";
Log.Error(exception, message);
throw new ServiceException($"{message}. Exception Message: {exception.Message}");
}
}
public async Task<DocumentInformationResponse?> UpdateDocumentInformation(UpdateDocumentInformationRequest request, Guid documentId, string userId)
{
DocumentInformation? documentInformation = null;
var modifiedDate = DateTime.UtcNow;
try{
documentInformation = await _documentInformationService.GetDocumentInformation(documentId);
if (documentInformation == null)
{
return null;
}
var updatedDocumentInformation = new DocumentInformation
{
DocumentId = documentId,
DocumentBlobId = documentInformation.DocumentBlobId,
TraceId = documentInformation.TraceId,
Source = documentInformation.Source,
DataSource = documentInformation.DataSource,
CreatedDate = documentInformation.CreatedDate,
Name = request.Name,
Type = request.Type,
PersonNumbers = request.PersonNumbers,
OrganizationNumbers = request.OrganizationNumbers,
AccountNumber = request.AccountNumber,
Attributes = request.Attributes,
UploadDate = documentInformation.UploadDate,
LastModifiedDate = modifiedDate,
FileSize = documentInformation.FileSize,
ScanStatus = documentInformation.ScanStatus
};
var docInfo = await _documentInformationService.UpdateDocumentInformation(documentId, updatedDocumentInformation);
await _documentAuditService.CreateDocumentAudits(documentInformation, updatedDocumentInformation, false, modifiedDate, userId);
return ConvertDocumentInformationToDocumentInformationResponse(docInfo);
}
catch (DocumentInformationServiceException exception){
var message = $"Failure to Update Document Information for DocumentId: {documentId} with Name: {request.Name} of type: {request.Type} from source: {request.UpdateSource}. Exception Message: {exception.Message}";
Log.Error(exception, message);
throw new ServiceException(message);
}
catch (DocumentAuditServiceException exception){
await RollbackDocumentInformation(documentId, documentInformation);
var message = $"Failure to Create Document Audits. Exception Message: {exception.Message}";
Log.Error(exception, message);
throw new ServiceException(message);
}
catch (Exception exception){
var message = $"Failure to Update Document Information for DocumentId: {documentId} with Name: {request.Name} of type: {request.Type} from source: {request.UpdateSource}.";
Log.Error(exception, message);
throw new ServiceException($"{message} Exception Message: {exception.Message}");
}
}
public async Task<List<DocumentInformationResponse>> SearchDocumentInformation(DocumentInformationFilter filter, int? skip = null, int? take = null, List<SortableField>? sortDefinition = null)
{
try{
var documentInformationFound = await _documentInformationService.SearchDocumentInformation(filter, skip, take, sortDefinition);
return documentInformationFound.Select(ConvertDocumentInformationToDocumentInformationResponse).ToList();
}
catch (DocumentInformationServiceException exception){
Log.Error(exception, exception.Message);
throw new ServiceException(exception.Message);
}
catch (Exception exception){
const string message = "Failure to Search Document Information";
Log.Error(exception, message);
throw new ServiceException($"{message} Exception Message: {exception.Message}");
}
}
public async Task UpdateDocumentScanStatus(bool status, List<Guid> documentBlobIds, string userId)
{
foreach (var documentBlobId in documentBlobIds){
DocumentInformation? documentInformation = null;
var modifiedDate = DateTime.UtcNow;
try{
documentInformation = await _documentInformationService.GetDocumentInformationByBlobId(documentBlobId);
if (documentInformation == null)
{
continue;
}
var updatedDocumentInformation = new DocumentInformation
{
DocumentBlobId = documentBlobId,
DocumentId = documentInformation.DocumentId,
Source = documentInformation.Source,
DataSource = documentInformation.DataSource,
TraceId = documentInformation.TraceId,
Name = documentInformation.Name,
Type = documentInformation.Type,
PersonNumbers = documentInformation.PersonNumbers,
OrganizationNumbers = documentInformation.OrganizationNumbers,
AccountNumber = documentInformation.AccountNumber,
Attributes = documentInformation.Attributes,
UploadDate = documentInformation.UploadDate,
CreatedDate = documentInformation.CreatedDate,
FileSize = documentInformation.FileSize,
ScanStatus = status,
LastModifiedDate = modifiedDate
};
await _documentInformationService.UpdateDocumentInformation(updatedDocumentInformation.DocumentId,
updatedDocumentInformation);
await _documentAuditService.CreateDocumentAudits(documentInformation, updatedDocumentInformation, false,
modifiedDate, userId);
if (!status)
{
await NotifyAboutMaliciousFile(documentInformation);
}
}
catch (DocumentInformationServiceException exception){
Log.Error(exception, $"Failure to Update Document Scan Status for DocumentBlobId: {documentBlobId}");
}
catch (DocumentAuditServiceException exception){
await RollbackDocumentInformation(documentInformation.DocumentId, documentInformation);
var message = $"Failure to Create Document Audits. Exception Message: {exception.Message}";
Log.Error(exception, message);
}
catch (Exception exception){
Log.Error(exception,$"Failure to Update Document Scan Status for DocumentBlobId: {documentBlobId}, caused by unexpected exception");
}
}
}
public async Task<long> CountDocuments(DocumentInformationFilter filter)
{
try
{
return await _documentInformationService.CountDocuments(filter);
}
catch (DocumentInformationServiceException exception)
{
Log.Error(exception, exception.Message);
throw new ServiceException(exception.Message);
}
catch (Exception exception)
{
const string message = "Failure to Count Document Information";
Log.Error(exception, message);
throw new ServiceException($"{message} Exception Message: {exception.Message}");
}
}
public async Task<List<Guid>> GetDocumentsForCleanup(GetDocumentCleanupRequest request)
{
try
{
return await _documentInformationService.GetDocumentsForCleanup(request);
}
catch (DocumentInformationServiceException exception)
{
Log.Error(exception, exception.Message);
throw new ServiceException(exception.Message);
}
catch (Exception exception)
{
const string message = "Failure GetDocumentsForCleanup";
Log.Error(exception, message);
throw new ServiceException($"{message} Exception Message: {exception.Message}");
}
}
private async Task RollBackDocumentContent(Guid docBlobId)
{
try{
Log.Information($"Removing {docBlobId} from Blob Storage, as part of a rollback transaction.");
await _blobApiService.DeleteDocumentContent(docBlobId);
}
catch (Exception inception){
Log.ForContext(Enum.GetName(typeof(PropertyEnum), PropertyEnum.Group),
Enum.GetName(typeof(GroupEnum), GroupEnum.ITDevelopment))
.Error(inception,
$"Failed to remove {docBlobId} from Blob Storage, an inconsistency in data now exists between DocumentInformation and Content. Exception Message: {inception.Message}");
}
}
private async Task RollbackDocumentInformation(Guid documentId, DocumentInformation documentInformation)
{
try{
Log.Information(
$"Reverting Document Information for DocumentId:{documentId}, as part of a rollback transaction.");
await _documentInformationService.UpdateDocumentInformation(documentId, documentInformation);
}
catch (Exception inception){
Log.ForContext(Enum.GetName(typeof(PropertyEnum), PropertyEnum.Group),
Enum.GetName(typeof(GroupEnum), GroupEnum.ITDevelopment))
.Error($"Failed to update document information for DocumentId:{documentId}, an inconsistency in data now exists between DocumentInformation. DocumentInformation should be updated to: {JsonSerializer.Serialize(documentInformation)} Exception Message: {inception.Message}");
}
}
private static BigInteger CalculateFileSize(byte[] bytes)
{
var fileBase64Value = Convert.ToBase64String(bytes);
var size = fileBase64Value.Length / 4 * 3;
if (fileBase64Value.EndsWith("=="))
size -= 2;
else if (fileBase64Value.EndsWith("=")) size -= 1;
return size;
}
private static DocumentInformationResponse ConvertDocumentInformationToDocumentInformationResponse(
DocumentInformation updatedDocumentInformation)
{
return new DocumentInformationResponse
{
DocumentId = updatedDocumentInformation.DocumentId,
DocumentBlobId = updatedDocumentInformation.DocumentBlobId,
TraceId = updatedDocumentInformation.TraceId,
Name = updatedDocumentInformation.Name,
Type = updatedDocumentInformation.Type,
Source = updatedDocumentInformation.Source,
DataSource = updatedDocumentInformation.DataSource,
FileSize = updatedDocumentInformation.FileSize,
ScanStatus = updatedDocumentInformation.ScanStatus,
PersonNumbers = updatedDocumentInformation.PersonNumbers,
OrganizationNumbers = updatedDocumentInformation.OrganizationNumbers,
AccountNumber = updatedDocumentInformation.AccountNumber,
Attributes = updatedDocumentInformation.Attributes,
UploadDate = updatedDocumentInformation.UploadDate,
LastModifiedDate = updatedDocumentInformation.LastModifiedDate,
CreatedDate = updatedDocumentInformation.CreatedDate
};
}
private async Task NotifyAboutMaliciousFile(DocumentInformation documentInformation)
{
try
{
var options = new JsonSerializerOptions { WriteIndented = true };
var documentAttributesSerialized =
JsonSerializer.Serialize(
documentInformation.Attributes,
new JsonSerializerOptions { WriteIndented = true }
);
var emailRequest = new EmailRequest()
{
Subject = "DocumentApi: Malicious File Reported",
Recipient = _apiConfiguration.MaliciousFileFoundEmailList,
Body = "This is an auto generated message. <br>" +
"<br>" +
"Azure Defender has returned a malicious file from scan<br>" +
"<br>" +
"Document Attributes:<br>" +
$"{documentAttributesSerialized}"
};
await _notificationApiService.SendEmailAsync(emailRequest);
}
catch (Exception ex)
{
var message = $"Failure to send email. Azure Defender returned a file as malicious. DocumentInformation: {JsonSerializer.Serialize(documentInformation)}. Exception Message: {ex.Message}";
Log.ForContext(Enum.GetName(typeof(PropertyEnum), PropertyEnum.Group),
Enum.GetName(typeof(GroupEnum), GroupEnum.ITDevelopment)).Error(ex, message);
throw new ServiceException(message);
}
}
public delegate bool DocumentIsForMemberStrategy(DocumentInformation document, long personOrOrgNumber);
public static class Ownership
{
public static class Unchecked
{
public static DocumentIsForMemberStrategy Strategy => DocumentIsForMember;
private static bool DocumentIsForMember(DocumentInformation document, long personOrOrgNumber)
{
var personOrOrgNumbers = (document.PersonNumbers ?? Array.Empty<string>())
.Concat(document.OrganizationNumbers ?? Array.Empty<string>())
.Select(n => long.TryParse(n, out var number) ? number : -1);
Log.Information("checking {personOrOrgNumber} on {document.DocumentId} in {personOrOrgNumbers} ignored", personOrOrgNumber, document.DocumentId, JsonSerializer.Serialize(personOrOrgNumbers));
return true;
}
}
public static class CheckPersonOrOrgNumber
{
public static DocumentIsForMemberStrategy Strategy => DocumentIsForMember;
private static bool DocumentIsForMember(DocumentInformation document, long personOrOrgNumber) => (document.PersonNumbers ?? Array.Empty<string>())
.Concat(document.OrganizationNumbers ?? Array.Empty<string>())
.Select(n => long.TryParse(n, out var number) ? number : -1)
.Contains(personOrOrgNumber);
}
}
}
@@ -0,0 +1,9 @@
namespace LCU.API.Internal.Document.Services;
public interface IBlobApiService
{
Task<byte[]> GetDocumentContent(Guid? documentBlobId);
Task<Guid> UploadDocumentContent(byte[] content);
Task UpdateDocumentContent(Guid? documentBlobId, byte[] content);
Task DeleteDocumentContent(Guid? documentBlobId);
}
@@ -0,0 +1,15 @@
using LCU.API.Internal.DocumentLib.Containers;
using LCU.Library.Internal.DocumentModels.Requests;
using LCU.Library.Internal.DocumentModels.Responses;
namespace LCU.API.Internal.Document.Services;
public interface IDocumentAuditService
{
Task<List<DocumentAuditResponse>> GetDocumentAudits(params Guid[] documentIds);
Task<IEnumerable<DocumentAudit>> CreateDocumentAudits(DocumentInformation oldDocumentInformation, DocumentInformation newDocumentInformation, bool wasDocumentContentUpdated, DateTime updateDate, string userId);
public Task<IEnumerable<DocumentAudit>> CreateDocumentUploadedAudit(Guid documentId, DateTime uploadDate);
public Task<IEnumerable<DocumentAudit>> CreateDocumentDeletedAudit(Guid documentId, DateTime deleteDate);
public Task<IEnumerable<DocumentAudit>> CreateDocumentContentDeletedAudit(Guid documentId, DateTime deleteDate, string? reason);
Task AuditAction(Guid documentId, string userId, string actionSource, DocumentAction action);
}
@@ -0,0 +1,17 @@
using LCU.API.Internal.DocumentLib.Containers;
using LCU.Library.Internal.DocumentIngestorModels.Models.Pagination;
using LCU.Library.Internal.DocumentModels.Requests.Search;
namespace LCU.API.Internal.Document.Services;
public interface IDocumentInformationService
{
Task<DocumentInformation?> GetDocumentInformation(Guid documentId);
Task<DocumentInformation?> GetDocumentInformationByBlobId(Guid? documentBlobId);
Task DeleteDocumentInformation(Guid documentId);
Task<DocumentInformation> UploadDocumentInformation(DocumentInformation information);
Task<DocumentInformation?> UpdateDocumentInformation(Guid documentId, DocumentInformation information);
Task<List<DocumentInformation>> SearchDocumentInformation(DocumentInformationFilter filter, int? skip = null, int? take = null, List<SortableField>? sortDefinition = null);
Task<long> CountDocuments(DocumentInformationFilter filter);
Task<List<Guid>> GetDocumentsForCleanup(GetDocumentCleanupRequest request);
}
@@ -0,0 +1,25 @@
using LCU.Library.Internal.DocumentIngestorModels.Models.Pagination;
using LCU.Library.Internal.DocumentModels.Requests;
using LCU.Library.Internal.DocumentModels.Requests.Search;
using LCU.Library.Internal.DocumentModels.Responses;
namespace LCU.API.Internal.Document.Services;
public interface IDocumentService
{
Task<DocumentResponse?> GetDocument(Guid documentId);
Task<DocumentResponse?> GetDocument(long personOrOrgNumber, Guid documentId);
Task<byte[]?> GetDocumentByBlobId(Guid documentBlobId);
Task<byte[]?> GetDocumentByBlobId(long personOrOrgNumber, Guid documentBlobId);
Task<List<DocumentInformationResponse>> SearchDocumentInformation(DocumentInformationFilter filter, int? skip = null, int? take = null, List<SortableField>? sortDefinition = null);
Task DeleteDocument(Guid documentId);
Task DeleteDocumentContent(DeleteDocumentContentRequest request);
Task<DocumentInformationResponse> UploadDocument(DocumentRequest request);
Task<DocumentInformationResponse?> GetDocumentInformation(Guid documentId);
Task<DocumentInformationResponse?> UpdateDocument(UpdateDocumentRequest request, Guid documentId, string userId);
Task<DocumentInformationResponse?> UpdateDocumentContent(UpdateDocumentContentRequest request, Guid documentId, string userId);
Task<DocumentInformationResponse?> UpdateDocumentInformation(UpdateDocumentInformationRequest request, Guid documentId, string userId);
Task UpdateDocumentScanStatus(bool status, List<Guid> documentBlobIds, string userId);
Task<long> CountDocuments(DocumentInformationFilter filter);
Task<List<Guid>> GetDocumentsForCleanup(GetDocumentCleanupRequest request);
}
@@ -0,0 +1,9 @@
using LCU.API.Internal.NotificationsApiLib.Models;
namespace LCU.API.Internal.Document.Services
{
public interface INotificationApiService
{
Task SendEmailAsync(EmailRequest emailRequest);
}
}
@@ -0,0 +1,30 @@
using LCU.API.Internal.DocumentLib.DataAccess;
using LCU.API.Internal.NotificationsApiLib.Models;
using LCU.Library.Internal.Exceptions;
using Serilog;
namespace LCU.API.Internal.Document.Services
{
public class NotificationApiService : INotificationApiService
{
private readonly INotificationApiDao _notificationServiceApiDao;
public NotificationApiService(INotificationApiDao notificationApiServiceDao)
{
_notificationServiceApiDao = notificationApiServiceDao;
}
public async Task SendEmailAsync(EmailRequest emailRequest)
{
try
{
await _notificationServiceApiDao.SendEmailAsync(emailRequest);
}
catch (Exception exception)
{
Log.Error(exception, $"Failure to send email. Subject: {emailRequest.Subject}; Recipients: {String.Join(", ", emailRequest.Recipient)}");
throw new ServiceException($"Failure to send email. Subject: {emailRequest.Subject}; Recipients: {String.Join(", ", emailRequest.Recipient)}; Exception Message: {exception.Message}");
}
}
}
}
@@ -0,0 +1,58 @@
{
"AzureAd": {
"ClientId": "c92072ab-987f-4b67-9923-730e06b2766f",
"Instance": "https://login.microsoftonline.com/",
"Domain": "landmarkcu.onmicrosoft.com",
"TenantId": "63cd1554-2783-4a86-ae50-b9e6c21c9499",
"CallbackPath": "/signin-oidc"
},
"AzureKeyVaultUrl": "qa-DocumentApi-kv",
"ClientId": "c92072ab-987f-4b67-9923-730e06b2766f",
"ClientSecret": "OjS8Q~TuByD~pylYMu5pbw9UGNG5fldT6eP9YcBI",
"TenantId": "63cd1554-2783-4a86-ae50-b9e6c21c9499",
"Swagger": {
"OAuthScope": "api://c92072ab-987f-4b67-9923-730e06b2766f/user.groups"
},
"Primary": {
"AppName": "Document Api",
"SeqUrl": "https://qa-seq.lcu-internal.com",
"SeqApiKey": "ftd9RlWVEI1ctg3dqusa",
"FileUploadCutoffHours": -2,
"IsAksDeploy": "false",
"CorsAllowedOrigins": [
"http://lcu-internal.com",
"https://lcu-internal.com",
"https://qa-api.lcu-internal.com"
],
"MaliciousFileFoundEmailList": [
"[email protected]",
"[email protected]"
]
},
"BlobStorage": {
"connectionString": "VALUE-IN-KEYVAULT",
"containerName": "documents"
},
"MongoDatabase": {
"connectionString": "VALUE-IN-KEYVAULT",
"databaseName": "DocumentsApi",
"documentContainerName": "DocumentsInformation",
"auditContainerName": "AuditInformation"
},
"ApiSecurityConfiguration": {
"ApiKeyAuthenticationFailureBehavior": "Error",
"ExternalApiKeySet": {
"DocumentTypeUploader": "VALUE-IN-KEYVAULT",
"DocumentTypeRetriever": "VALUE-IN-KEYVAULT",
"DocumentTypeCleanup": "VALUE-IN-KEYVAULT"
},
"InternalApiKeySet": {}
},
"NotificationApiConfiguration": {
"Url": "https://qa-notificationsapi.lcu-internal.com",
"Email": "/api/Email"
},
"FeatureManagement": {
"GetDocumentCheckOwnership": true
}
}
@@ -0,0 +1,7 @@
namespace LCU.API.Internal.DocumentLib.Configurations;
public class BlobStorageConfiguration
{
public string ConnectionString { get; set; }
public string ContainerName { get; set; }
}
@@ -0,0 +1,11 @@
namespace LCU.API.Internal.DocumentLib.Configurations;
public class DatabaseConfiguration
{
public string ConnectionString { get; set; }
public string DatabaseName { get; set; }
public string DocumentContainerName { get; set; }
public string AuditContainerName { get; set; }
}
@@ -0,0 +1,17 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace LCU.API.Internal.DocumentLib.Configurations;
public class NotificationApiConfiguration
{
public string Url { get; set; }
public string Email { get; set; }
[NotMapped]
public string EmailUrl
{
get
{
return Url + Email;
}
}
}
@@ -0,0 +1,55 @@
using System;
using LCU.Library.Internal.DocumentModels.Requests;
using MongoDB.Bson.Serialization.Attributes;
namespace LCU.API.Internal.DocumentLib.Containers;
[BsonKnownTypes(typeof(DocumentFieldAudit), typeof(DocumentActionAudit))]
public abstract class DocumentAudit
{
protected DocumentAudit(Guid documentId, string source, DateTime date, string userId)
{
DocumentId = documentId;
Source = source;
Date = date;
UserId = userId;
}
public Guid DocumentId { get; set; }
public string Source { get; set; }
[BsonDateTimeOptions(Kind = DateTimeKind.Utc)]
public DateTime Date { get; set; }
public string UserId { get; set; }
}
[BsonIgnoreExtraElements]
public class DocumentFieldAudit : DocumentAudit
{
public DocumentFieldAudit(Guid documentId, string source, DateTime date, string userId, string field, object previousValue, object currentValue)
: base(documentId, source, date, userId)
{
Field = field;
PreviousValue = previousValue;
CurrentValue = currentValue;
}
public string Field { get; set; }
public object PreviousValue { get; set; }
public object CurrentValue { get; set; }
}
[BsonIgnoreExtraElements]
public class DocumentActionAudit : DocumentAudit
{
public DocumentActionAudit(Guid documentId, string source, DateTime date, string userId, DocumentAction action, string description)
: base(documentId, source, date, userId)
{
Action = action;
ActionDescription = description;
}
public DocumentAction Action { get; set; }
public string ActionDescription { get; set; }
}
@@ -0,0 +1,40 @@
using System;
using MongoDB.Bson;
using MongoDB.Bson.IO;
using MongoDB.Bson.Serialization.Conventions;
namespace LCU.API.Internal.DocumentLib.Containers;
public class DocumentAuditDiscriminatorConvention : IDiscriminatorConvention
{
public static string Discriminator = StandardDiscriminatorConvention.Scalar.ElementName;
public string ElementName => Discriminator;
public Type GetActualType(IBsonReader bsonReader, Type nominalType)
{
var bookmark = bsonReader.GetBookmark();
try
{
bsonReader.ReadStartDocument();
if (!bsonReader.FindElement(ElementName)) return typeof(DocumentFieldAudit);
return bsonReader.ReadString() switch
{
nameof(DocumentFieldAudit) => typeof(DocumentFieldAudit),
nameof(DocumentActionAudit) => typeof(DocumentActionAudit),
_ => throw new NotSupportedException()
};
}
finally
{
bsonReader.ReturnToBookmark(bookmark);
}
}
public BsonValue GetDiscriminator(Type nominalType, Type actualType)
{
return actualType.Name;
}
}
@@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using MongoDB.Bson.Serialization;
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Bson.Serialization.Serializers;
namespace LCU.API.Internal.DocumentLib.Containers;
[BsonIgnoreExtraElements]
public class DocumentInformation
{
public Guid DocumentId { get; set; }
public Guid? DocumentBlobId { get; set; }
public Guid? TraceId { get; set; }
public string Name { get; set; }
public string Type { get; set; }
public string Source { get; set; }
public string DataSource { get; set; }
[BsonSerializer(typeof(BigIntSerializer))]
public BigInteger FileSize { get; set; }
public bool? ScanStatus { get; set; }
[BsonDateTimeOptions(Kind = DateTimeKind.Utc)]
public DateTime UploadDate { get; set; }
[BsonDateTimeOptions(Kind = DateTimeKind.Utc)]
public DateTime? LastModifiedDate { get; set; }
[BsonDateTimeOptions(Kind = DateTimeKind.Utc)]
public DateTime CreatedDate { get; set; }
public string[] PersonNumbers { get; set; }
public string[] OrganizationNumbers { get; set; }
public string AccountNumber { get; set; }
public Dictionary<string, string> Attributes { get; set; }
}
public class BigIntSerializer : SerializerBase<BigInteger>
{
public override BigInteger Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
{
var val = context.Reader.ReadString();
return BigInteger.Parse(val);
}
public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, BigInteger value)
{
context.Writer.WriteString(value.ToString());
}
}
@@ -0,0 +1,44 @@
using System;
using System.IO;
using System.Threading.Tasks;
using Azure;
using Azure.Storage.Blobs;
using Azure.Storage.Blobs.Models;
namespace LCU.API.Internal.DocumentLib.DataAccess;
public class BlobApiDao : IBlobApiDao
{
private readonly BlobContainerClient _client;
public BlobApiDao(IBlobStorageClient storageClient)
{
_client = storageClient.GetClient();
}
public async Task<Guid> SaveDocumentToBlob(byte[] fileData)
{
var blobDocumentId = Guid.NewGuid();
using var stream = new MemoryStream(fileData, false);
await _client.GetBlobClient(blobDocumentId.ToString()).UploadAsync(stream);
return blobDocumentId;
}
public async Task UpdateDocumentInBlob(Guid blobDocumentId, byte[] fileData)
{
using var stream = new MemoryStream(fileData, false);
await _client.GetBlobClient(blobDocumentId.ToString()).UploadAsync(stream, true);
}
public async Task<Response> RemoveDocumentFromBlob(Guid blobDocumentId)
{
return await _client.GetBlobClient(blobDocumentId.ToString())
.DeleteAsync(DeleteSnapshotsOption.IncludeSnapshots);
}
public async Task<Response<BlobDownloadStreamingResult>> GetDocumentFromBlob(Guid blobDocumentId)
{
return await _client.GetBlobClient(blobDocumentId.ToString()).DownloadStreamingAsync();
}
}
@@ -0,0 +1,20 @@
using Azure.Storage.Blobs;
using LCU.API.Internal.DocumentLib.Configurations;
namespace LCU.API.Internal.DocumentLib.DataAccess;
public class BlobStorageClient : IBlobStorageClient
{
private readonly BlobContainerClient _containerClient;
public BlobStorageClient(BlobStorageConfiguration config)
{
var serviceClient = new BlobServiceClient(config.ConnectionString);
_containerClient = serviceClient.GetBlobContainerClient(config.ContainerName);
}
public BlobContainerClient GetClient()
{
return _containerClient;
}
}
@@ -0,0 +1,14 @@
using System;
using System.Threading.Tasks;
using Azure;
using Azure.Storage.Blobs.Models;
namespace LCU.API.Internal.DocumentLib.DataAccess;
public interface IBlobApiDao
{
Task<Guid> SaveDocumentToBlob(byte[] fileData);
Task UpdateDocumentInBlob(Guid blobDocumentId, byte[] fileData);
Task<Response> RemoveDocumentFromBlob(Guid blobDocumentId);
Task<Response<BlobDownloadStreamingResult>> GetDocumentFromBlob(Guid blobDocumentId);
}
@@ -0,0 +1,8 @@
using Azure.Storage.Blobs;
namespace LCU.API.Internal.DocumentLib.DataAccess;
public interface IBlobStorageClient
{
BlobContainerClient GetClient();
}
@@ -0,0 +1,10 @@
using LCU.API.Internal.NotificationsApiLib.Models;
using System.Threading.Tasks;
namespace LCU.API.Internal.DocumentLib.DataAccess
{
public interface INotificationApiDao
{
Task SendEmailAsync(EmailRequest emailRequest);
}
}
@@ -0,0 +1,23 @@
using LCU.API.Internal.DocumentLib.Configurations;
using LCU.API.Internal.DocumentLib.Utilities;
using LCU.API.Internal.NotificationsApiLib.Models;
using System.Threading.Tasks;
using System;
namespace LCU.API.Internal.DocumentLib.DataAccess
{
public class NotificationApiDao : INotificationApiDao
{
private readonly NotificationApiConfiguration _notificationApiConfiguration;
public NotificationApiDao(NotificationApiConfiguration notificationApiConfiguration)
{
_notificationApiConfiguration = notificationApiConfiguration;
}
public async Task SendEmailAsync(EmailRequest emailRequest)
{
await HttpUtilities.PostAsync<EmailRequest>(new Uri(_notificationApiConfiguration.EmailUrl),
emailRequest);
}
}
}
@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.Storage.Blobs" Version="12.13.0" />
<PackageReference Include="LCU.API.Internal.NotificationsApiLib" Version="2.1.0" />
<PackageReference Include="LCU.Library.Internal.Exceptions" Version="1.0.0" />
<PackageReference Include="Microsoft.Azure.Cosmos" Version="3.30.1" />
<PackageReference Include="MongoDB.Driver" Version="2.20.0" />
<PackageReference Include="Serilog" Version="2.11.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\LCU.Library.Internal.DocumentModels\LCU.Library.Internal.DocumentModels.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,19 @@
using LCU.API.Internal.DocumentLib.Configurations;
using MongoDB.Driver;
namespace LCU.API.Internal.DocumentLib.Repositories;
public class DataClient : IDataClient
{
private readonly MongoClient _client;
public DataClient(DatabaseConfiguration config)
{
_client = new MongoClient(config.ConnectionString);
}
public MongoClient GetClient()
{
return _client;
}
}
@@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using LCU.API.Internal.DocumentLib.Containers;
using LCU.API.Internal.DocumentLib.Utilities;
using MongoDB.Driver;
namespace LCU.API.Internal.DocumentLib.Repositories;
public class DocumentAuditRepository : IDocumentAuditRepository
{
private readonly IMongoCollection<DocumentAudit> _documentAudits;
public DocumentAuditRepository(IMongoCollectionFactory collectionFactory)
{
_documentAudits = collectionFactory.GetCollection<DocumentAudit>();
}
public async Task<List<DocumentAudit>> GetDocumentAudits(params Guid[] documentIds)
{
return await (await _documentAudits.FindAsync(doc => documentIds.Contains(doc.DocumentId))).ToListAsync();
}
public async Task<IEnumerable<DocumentAudit>> SaveDocumentAudits(params DocumentAudit[] documentAudit)
{
await _documentAudits.InsertManyAsync(documentAudit);
return documentAudit;
}
}
@@ -0,0 +1,269 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using LCU.API.Internal.DocumentLib.Containers;
using LCU.API.Internal.DocumentLib.Utilities;
using LCU.Library.Internal.DocumentIngestorModels.Models.Pagination;
using LCU.Library.Internal.DocumentModels.Requests.Search;
using Microsoft.Azure.Cosmos.Linq;
using MongoDB.Bson;
using MongoDB.Driver;
namespace LCU.API.Internal.DocumentLib.Repositories;
public class DocumentInformationRepository : IDocumentInformationRepository
{
private readonly IMongoCollection<DocumentInformation> _documents;
public DocumentInformationRepository(IMongoCollectionFactory collectionFactory)
{
_documents = collectionFactory.GetCollection<DocumentInformation>();
}
public async Task<DocumentInformation> GetDocumentInformation(Guid documentId)
{
return await _documents.Find(doc => doc.DocumentId == documentId)
.SingleOrDefaultAsync();
}
public async Task<List<DocumentInformation>> SearchDocumentInformation(DocumentInformationFilter filter, int? skip = null, int? take = null, List<SortableField>? sortDefinition = null)
{
var pipelineFilter = FilterFactory(filter);
var sorter = Sorter<DocumentInformation>.GetSorter(sortDefinition);
if (sorter != null) pipelineFilter = pipelineFilter.Sort(sorter);
if (skip.HasValue) pipelineFilter = pipelineFilter.Skip(skip.Value);
if (take.HasValue) pipelineFilter = pipelineFilter.Limit(take.Value);
var documentsInformation = await _documents.AggregateAsync(pipelineFilter);
return await documentsInformation.ToListAsync();
}
public async Task<long> CountDocuments(DocumentInformationFilter filter)
{
return (await _documents.AggregateAsync(FilterFactory(filter).Count())).Single().Count;
}
public async Task<DocumentInformation> GetDocumentInformationByBlobId(Guid documentBlobId)
{
return await _documents
.Find(doc => doc.DocumentBlobId == documentBlobId)
.SingleOrDefaultAsync();
}
public async Task<DocumentInformation> SaveDocumentInformation(DocumentInformation document)
{
await _documents.InsertOneAsync(document);
return document;
}
public async Task<DocumentInformation> UpdateDocumentInformation(Guid documentId, DocumentInformation newDocument)
{
await _documents.FindOneAndReplaceAsync(doc => doc.DocumentId == documentId, newDocument);
return newDocument;
}
public async Task<DocumentInformation> RemoveDocumentInformation(Guid documentId)
{
return await _documents.FindOneAndDeleteAsync(document => document.DocumentId == documentId);
}
public async Task<List<Guid>> GetDocumentsForCleanup(GetDocumentCleanupRequest request)
{
IList<FilterDefinition<DocumentInformation>> filterConcat = new List<FilterDefinition<DocumentInformation>>();
foreach(var documentCodeDetail in request.DocumentCodeDetailInfos)
{
var adjustedDate = DateTime.Now.AddMonths(documentCodeDetail.InternalRetentionMonths * -1);
var typeFilter = Builders<DocumentInformation>.Filter.Eq(x => x.Type, documentCodeDetail.DocumentCode);
var dateFilter = Builders<DocumentInformation>.Filter.Lt(x => x.CreatedDate, adjustedDate);
var groupFilter = Builders<DocumentInformation>.Filter.And(typeFilter, dateFilter);
filterConcat.Add(groupFilter);
}
var filter = Builders<DocumentInformation>.Filter;
var oneConcat = filter.Empty;
oneConcat &= filter.Or(filterConcat);
var nullFilter = Builders<DocumentInformation>.Filter.Ne(x => x.DocumentBlobId, null);
oneConcat &= filter.And(nullFilter);
var pipelineFilter = new EmptyPipelineDefinition<DocumentInformation>()
.Match(oneConcat)
.Limit(request.Limit);
var documentsInformation = await _documents.AggregateAsync(pipelineFilter);
return (await documentsInformation.ToListAsync()).Select(document => document.DocumentId).ToList();
}
private PipelineDefinition<DocumentInformation, DocumentInformation> FilterFactory(DocumentInformationFilter filter)
{
var builder = Builders<DocumentInformation>.Filter;
IList<FilterDefinition<DocumentInformation>> stageOneFiltersAnd = new List<FilterDefinition<DocumentInformation>>();
IList<FilterDefinition<DocumentInformation>> stageOneFiltersOr = new List<FilterDefinition<DocumentInformation>>();
IList<FilterDefinition<DocumentInformation>> stageTwoFiltersAnd = new List<FilterDefinition<DocumentInformation>>();
IList<FilterDefinition<DocumentInformation>> stageTwoFiltersOr = new List<FilterDefinition<DocumentInformation>>();
if (!string.IsNullOrWhiteSpace(filter.Name))
{
stageOneFiltersAnd.Add(builder.Eq("Name", filter.Name));
}
if (!string.IsNullOrWhiteSpace(filter.Type))
{
stageOneFiltersAnd.Add(builder.Eq("Type", filter.Type));
}
if (filter.TypeContainsAny != null && filter.TypeContainsAny.Any())
{
stageOneFiltersAnd.Add(builder.In("Type", filter.TypeContainsAny));
}
if (filter.ScanStatus != null)
{
stageOneFiltersAnd.Add(builder.Eq("ScanStatus", filter.ScanStatus));
}
if (!string.IsNullOrWhiteSpace(filter.Source))
{
stageOneFiltersAnd.Add(builder.Eq("Source", filter.Source));
}
if (!string.IsNullOrWhiteSpace(filter.DataSource))
{
stageOneFiltersAnd.Add(builder.Eq("DataSource", filter.DataSource));
}
if (filter.TraceId != null)
{
stageOneFiltersAnd.Add(builder.Eq("TraceId", filter.TraceId));
}
if (!string.IsNullOrWhiteSpace(filter.PersonNumbersEquals))
{
stageOneFiltersAnd.Add(builder.Eq("PersonNumbers", filter.PersonNumbersEquals));
}
if (filter.PersonNumbersContains != null && filter.PersonNumbersContains.Any())
{
stageOneFiltersAnd.Add(builder.All("PersonNumbers", filter.PersonNumbersContains));
}
if (filter.PersonNumbersContainsAny != null && filter.PersonNumbersContainsAny.Any())
{
stageOneFiltersAnd.Add(builder.In("PersonNumbers", filter.PersonNumbersContainsAny));
}
if (filter.OrganizationNumbersEquals != null && filter.OrganizationNumbersEquals.Any())
{
stageOneFiltersAnd.Add(builder.Eq("OrganizationNumbers", filter.OrganizationNumbersEquals));
}
if (filter.OrganizationNumbersContains != null && filter.OrganizationNumbersContains.Any())
{
stageOneFiltersAnd.Add(builder.All("OrganizationNumbers", filter.OrganizationNumbersContains));
}
if (filter.OrganizationNumbersContainsAny != null && filter.OrganizationNumbersContainsAny.Any())
{
stageOneFiltersAnd.Add(builder.In("OrganizationNumbers", filter.OrganizationNumbersContainsAny));
}
if (filter.AccountNumberEquals != null && filter.AccountNumberEquals.Any())
{
stageOneFiltersAnd.Add(builder.Eq("AccountNumber", filter.AccountNumberEquals));
}
if (filter.AccountNumberContainsAny != null && filter.AccountNumberContainsAny.Any())
{
stageOneFiltersAnd.Add(builder.In("AccountNumber", filter.AccountNumberContainsAny));
}
if (filter.AttributesEquals != null && filter.AttributesEquals.Any())
{
foreach (var keyValuePair in filter.AttributesEquals)
{
stageOneFiltersAnd.Add(builder.Eq($"Attributes.{keyValuePair.Key.Trim()}", keyValuePair.Value.Trim()));
}
}
if (filter.AttributesContains is not null)
{
foreach (var keyValuePair in filter.AttributesContains)
{
var queryExpr =
new BsonRegularExpression(
new Regex($"\\b({keyValuePair.Value.Trim()})\\b", RegexOptions.IgnoreCase));
stageTwoFiltersAnd.Add(builder.Regex($"Attributes.{keyValuePair.Key.Trim()}", queryExpr));
}
}
if (filter.AttributesContainsAny is not null)
{
foreach (var keyValuePair in filter.AttributesContainsAny)
{
foreach (var value in keyValuePair.Value.Split(','))
{
var queryExpr = new BsonRegularExpression(new Regex($"^{value.Trim()}$", RegexOptions.IgnoreCase));
stageTwoFiltersOr.Add(builder.Regex($"Attributes.{keyValuePair.Key.Trim()}", queryExpr));
}
}
}
if (filter.CreatedDateMinimum != null)
{
stageOneFiltersAnd.Add(builder.Gte("CreatedDate", filter.CreatedDateMinimum));
}
if (filter.CreatedDateMaximum != null)
{
stageOneFiltersAnd.Add(builder.Lte("CreatedDate", filter.CreatedDateMaximum));
}
if (filter.UploadDateMinimum != null)
{
stageOneFiltersAnd.Add(builder.Gte("UploadDate", filter.UploadDateMinimum));
}
if (filter.UploadDateMaximum != null)
{
stageOneFiltersAnd.Add(builder.Lte("UploadDate", filter.UploadDateMaximum));
}
if (filter.LastModifiedMinimum != null)
{
stageOneFiltersAnd.Add(builder.Gte("LastModifiedDate", filter.LastModifiedMinimum));
}
if (filter.LastModifiedDateMaximum != null)
{
stageOneFiltersAnd.Add(builder.Lte("LastModifiedDate", filter.LastModifiedDateMaximum));
}
var stageOneFilterConcat = builder.Empty;
if (stageOneFiltersAnd.Any())
{
stageOneFilterConcat &= builder.And(stageOneFiltersAnd);
}
if (stageOneFiltersOr.Any())
{
stageOneFilterConcat &= builder.Or(stageOneFiltersOr);
}
var stageTwoFilterConcat = builder.Empty;
if (stageTwoFiltersAnd.Any())
{
stageOneFilterConcat &= builder.And(stageTwoFiltersAnd);
}
if (stageTwoFiltersOr.Any())
{
stageTwoFilterConcat &= builder.Or(stageTwoFiltersOr);
}
return new EmptyPipelineDefinition<DocumentInformation>()
.Match(stageOneFilterConcat)
.Match(stageTwoFilterConcat);
}
}
@@ -0,0 +1,8 @@
using MongoDB.Driver;
namespace LCU.API.Internal.DocumentLib.Repositories;
public interface IDataClient
{
MongoClient GetClient();
}
@@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using LCU.API.Internal.DocumentLib.Containers;
namespace LCU.API.Internal.DocumentLib.Repositories;
public interface IDocumentAuditRepository
{
Task<List<DocumentAudit>> GetDocumentAudits(params Guid[] documentIds);
Task<IEnumerable<DocumentAudit>> SaveDocumentAudits(params DocumentAudit[] documentAudit);
}
@@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using LCU.API.Internal.DocumentLib.Containers;
using LCU.Library.Internal.DocumentIngestorModels.Models.Pagination;
using LCU.Library.Internal.DocumentModels.Requests.Search;
namespace LCU.API.Internal.DocumentLib.Repositories;
public interface IDocumentInformationRepository
{
Task<DocumentInformation> GetDocumentInformation(Guid documentId);
Task<DocumentInformation> GetDocumentInformationByBlobId(Guid documentBlobId);
Task<List<DocumentInformation>> SearchDocumentInformation(DocumentInformationFilter filter, int? skip = null, int? take = null, List<SortableField>? sortDefinition = null);
Task<DocumentInformation> SaveDocumentInformation(DocumentInformation document);
Task<DocumentInformation> UpdateDocumentInformation(Guid documentId, DocumentInformation newDocument);
Task<DocumentInformation> RemoveDocumentInformation(Guid documentId);
Task<long> CountDocuments(DocumentInformationFilter filter);
Task<List<Guid>> GetDocumentsForCleanup(GetDocumentCleanupRequest request);
}
@@ -0,0 +1,105 @@
using LCU.Library.Internal.Exceptions;
using Newtonsoft.Json;
using Serilog;
using System;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using JsonSerializer = System.Text.Json.JsonSerializer;
namespace LCU.API.Internal.DocumentLib.Utilities;
public class HttpUtilities
{
public static HttpClient CreateClient(Uri uri)
{
var handler = new HttpClientHandler
{
SslProtocols = System.Security.Authentication.SslProtocols.Tls12 |
System.Security.Authentication.SslProtocols.Tls11 |
System.Security.Authentication.SslProtocols.Tls,
ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => true
};
var httpClient = new HttpClient(handler)
{
Timeout = TimeSpan.FromSeconds(60),
BaseAddress = uri
};
return httpClient;
}
public static async Task PostAsync<TRequest>(Uri uri, TRequest request)
{
try
{
var client = CreateClient(uri);
var jsonBody = JsonSerializer.Serialize(request);
var content = new StringContent(jsonBody, Encoding.UTF8, "application/json");
var response = await client.PostAsync(uri, content);
if (!response.IsSuccessStatusCode)
{
throw new ServiceException($"StatusCode:{response.StatusCode}, " +
$"Content: {response.Content.ReadAsStringAsync().Result}");
}
}
catch (Exception exception)
{
Log.Error(exception, "Failure during PostAsync.");
throw new ServiceException($"Failure during Document Upload. Exception Message: {exception.Message}");
}
}
public static async Task<TResponse> PostAsync<TRequest, TResponse>(Uri uri, TRequest request)
{
try
{
var client = CreateClient(uri);
var jsonBody = JsonSerializer.Serialize(request);
var content = new StringContent(jsonBody, Encoding.UTF8, "application/json");
var response = await client.PostAsync(uri, content);
if (!response.IsSuccessStatusCode)
{
throw new ServiceException($"StatusCode:{response.StatusCode}, " +
$"Content: {response.Content.ReadAsStringAsync().Result}");
}
var stringResponse = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<TResponse>(stringResponse, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
}
catch (Exception exception)
{
Log.Error(exception, "Failure during PostAsync.");
throw new ServiceException($"Failure during PostAsync. Exception Message: {exception.Message}");
}
}
public static async Task<TResponse> GetAsync<TResponse>(string url)
{
try
{
var uri = new Uri(url);
var client = CreateClient(uri);
var response = await client.GetAsync(uri);
if (!response.IsSuccessStatusCode)
{
throw new ServiceException($"StatusCode:{response.StatusCode}, " +
$"Content: {response.Content.ReadAsStringAsync().Result}");
}
var stringResponse = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<TResponse>(stringResponse, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
}
catch (Exception exception)
{
Log.Error(exception, "Failure during GetAsync.");
throw new ServiceException($"Failure during GetAsync. Exception Message: {exception.Message}");
}
}
}
@@ -0,0 +1,8 @@
using MongoDB.Driver;
namespace LCU.API.Internal.DocumentLib.Utilities;
public interface IMongoCollectionFactory
{
IMongoCollection<T> GetCollection<T>();
}
@@ -0,0 +1,37 @@
using System;
using LCU.API.Internal.DocumentLib.Configurations;
using LCU.API.Internal.DocumentLib.Containers;
using LCU.API.Internal.DocumentLib.Repositories;
using MongoDB.Driver;
namespace LCU.API.Internal.DocumentLib.Utilities;
public class MongoCollectionFactory : IMongoCollectionFactory
{
private readonly IDataClient _mongoDataClient;
private readonly DatabaseConfiguration _config;
public MongoCollectionFactory(IDataClient mongoDataClient, DatabaseConfiguration config)
{
_mongoDataClient = mongoDataClient;
_config = config;
}
public IMongoCollection<T> GetCollection<T>()
{
return _mongoDataClient
.GetClient()
.GetDatabase(_config.DatabaseName)
.GetCollection<T>(CollectionNameFor<T>());
}
private string CollectionNameFor<T>()
{
return typeof(T) switch
{
var t when t == typeof(DocumentInformation) => _config.DocumentContainerName,
var t when t == typeof(DocumentAudit) => _config.AuditContainerName,
_ => throw new NotImplementedException()
};
}
}
@@ -0,0 +1,185 @@
using FluentAssertions;
using LCU.API.Internal.DocumentLib.Containers;
using LCU.API.Internal.DocumentLibTest.TestDoubles;
using LCU.Library.Internal.DocumentModels.Requests;
using MongoDB.Bson.Serialization;
using MongoDB.Driver;
namespace LCU.API.Internal.DocumentLibTest.Containers;
public class DocumentAuditDiscriminatorConventionTest : IDisposable
{
private readonly FakeMongoCollectionFactory _collectionFactory = new();
private readonly IMongoCollection<DocumentAudit> _collection;
public DocumentAuditDiscriminatorConventionTest()
{
_collection = _collectionFactory.GetCollection<DocumentAudit>();
}
private static bool _registered = false;
[SetUp]
public void Setup()
{
if (_registered) return;
BsonSerializer.RegisterDiscriminatorConvention
(
typeof(DocumentAudit),
new DocumentAuditDiscriminatorConvention()
);
_registered = true;
}
[Test]
public async Task FindAuditDocuments_DocumentMissingDiscriminator_DeserializesAsFieldAudit()
{
var audit = FieldAuditFactory();
await _collection.InsertOneAsync(audit);
await _collection.FindOneAndUpdateAsync
(
a => a.DocumentId == audit.DocumentId,
Builders<DocumentAudit>.Update.Unset(DocumentAuditDiscriminatorConvention.Discriminator)
);
var retrievedAudit = await
(await _collection.FindAsync(a => a.DocumentId == audit.DocumentId))
.SingleOrDefaultAsync();
Assert.IsInstanceOf<DocumentFieldAudit>(retrievedAudit);
retrievedAudit.Should().BeEquivalentTo(audit, a => a.Using(DateTimeComparison.Truncated));
}
[Test]
public async Task FindAuditDocuments_DiscriminatorIsFieldAudit_DeserializesAsFieldAudit()
{
var audit = FieldAuditFactory();
await _collection.InsertOneAsync(audit);
await _collection.FindOneAndUpdateAsync
(
a => a.DocumentId == audit.DocumentId,
Builders<DocumentAudit>.Update.Set
(
DocumentAuditDiscriminatorConvention.Discriminator,
nameof(DocumentFieldAudit)
)
);
var retrievedAudit = await
(await _collection.FindAsync(a => a.DocumentId == audit.DocumentId))
.SingleOrDefaultAsync();
Assert.IsInstanceOf<DocumentFieldAudit>(retrievedAudit);
retrievedAudit.Should().BeEquivalentTo(audit, a => a.Using(DateTimeComparison.Truncated));
}
[Test]
public async Task InsertFieldAuditDocuments_NewDocumentWithConventionRegistered_DiscriminatorIsFieldAudit()
{
var audit = FieldAuditFactory();
await _collection.InsertOneAsync(audit);
var audits = await
(await _collection.FindAsync
(
Builders<DocumentAudit>.Filter.Eq
(
DocumentAuditDiscriminatorConvention.Discriminator,
nameof(DocumentFieldAudit)
)
)
)
.ToListAsync();
audits.Should().ContainEquivalentOf(audit, a => a.Using(DateTimeComparison.Truncated));
}
[Test]
public async Task FindAuditDocuments_DiscriminatorIsActionAudit_DeserializesAsActionAudit()
{
var audit = ActionAuditFactory();
await _collection.InsertOneAsync(audit);
await _collection.FindOneAndUpdateAsync
(
a => a.DocumentId == audit.DocumentId,
Builders<DocumentAudit>.Update.Set
(
DocumentAuditDiscriminatorConvention.Discriminator,
nameof(DocumentActionAudit)
)
);
var retrievedAudit = await
(await _collection.FindAsync(a => a.DocumentId == audit.DocumentId))
.SingleOrDefaultAsync();
Assert.IsInstanceOf<DocumentActionAudit>(retrievedAudit);
retrievedAudit.Should().BeEquivalentTo(audit, a => a.Using(DateTimeComparison.Truncated));
}
[Test]
public async Task InsertActionAuditDocuments_NewDocumentWithConventionRegistered_DiscriminatorIsActionAudit()
{
var audit = ActionAuditFactory();
await _collection.InsertOneAsync(audit);
var audits = await
(await _collection.FindAsync
(
Builders<DocumentAudit>.Filter.Eq
(
DocumentAuditDiscriminatorConvention.Discriminator,
nameof(DocumentActionAudit)
)
)
)
.ToListAsync();
audits.Should().ContainEquivalentOf(audit, a => a.Using(DateTimeComparison.Truncated));
}
[Test]
public async Task FindAuditDocuments_UnknownDiscriminator_ThrowsException()
{
var audit = FieldAuditFactory();
await _collection.InsertOneAsync(audit);
await _collection.FindOneAndUpdateAsync
(
a => a.DocumentId == audit.DocumentId,
Builders<DocumentAudit>.Update.Set
(
DocumentAuditDiscriminatorConvention.Discriminator,
Guid.NewGuid().ToString()
)
);
Assert.ThrowsAsync<NotSupportedException>(() => _collection.FindAsync(a => a.DocumentId == audit.DocumentId));
}
private static DocumentFieldAudit FieldAuditFactory() => new
(
Guid.NewGuid(),
Guid.NewGuid().ToString(),
DateTime.UtcNow,
Guid.NewGuid().ToString(),
Guid.NewGuid().ToString(),
Guid.NewGuid(),
Guid.NewGuid()
);
private static DocumentActionAudit ActionAuditFactory() => new
(
Guid.NewGuid(),
Guid.NewGuid().ToString(),
DateTime.UtcNow,
Guid.NewGuid().ToString(),
DocumentAction.Download,
Guid.NewGuid().ToString()
);
public void Dispose()
{
_collectionFactory.Dispose();
}
}
@@ -0,0 +1,31 @@
using FluentAssertions;
using FluentAssertions.Equivalency;
namespace LCU.API.Internal.DocumentLibTest;
public static class DateTimeComparison
{
public static IEquivalencyStep Truncated { get; } = new TruncatedDateTimeStep();
public static DateTime Truncate(this DateTime date) => date.AddTicks(-(date.Ticks % 10000));
private class TruncatedDateTimeStep : IEquivalencyStep
{
public EquivalencyResult Handle
(
Comparands comparands,
IEquivalencyValidationContext context,
IEquivalencyValidator nestedValidator
)
{
if (comparands.Subject is not DateTime subject || comparands.Expectation is not DateTime expectation)
return EquivalencyResult.ContinueWithNext;
subject.Truncate()
.Should()
.Be(expectation.Truncate(), context.Reason.FormattedMessage, context.Reason.Arguments);
return EquivalencyResult.AssertionCompleted;
}
}
}
@@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.7.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
<PackageReference Include="Mongo2Go" Version="3.1.3" />
<PackageReference Include="Moq" Version="4.18.2" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
<PackageReference Include="NUnit.Analyzers" Version="3.3.0" />
<PackageReference Include="coverlet.collector" Version="3.1.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\LCU.API.Internal.DocumentLib\LCU.API.Internal.DocumentLib.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Repositories\" />
</ItemGroup>
</Project>
@@ -0,0 +1,124 @@
using FluentAssertions;
using LCU.API.Internal.DocumentLib.Containers;
using LCU.API.Internal.DocumentLib.Repositories;
using LCU.API.Internal.DocumentLibTest.TestDataFactories;
using LCU.API.Internal.DocumentLibTest.TestDoubles;
using LCU.Library.Internal.DocumentModels.Requests;
using MongoDB.Driver;
namespace LCU.API.Internal.DocumentLibTest.Repositories;
public class DocumentAuditRepositoryTest : IDisposable
{
private readonly IDocumentAuditRepository _repository;
private readonly FakeMongoCollectionFactory _collectionFactory = new();
public DocumentAuditRepositoryTest()
{
_repository = new DocumentAuditRepository(_collectionFactory);
}
[Test]
public async Task GetDocumentAudits_FieldAuditRecordsExistForId_ReturnsMatchedLogsById()
{
var audits = EnumerableDataSeriesFactory.CreateSeries(() => FieldAuditFactory(), 5).ToArray();
await _collectionFactory.GetCollection<DocumentAudit>().InsertManyAsync(audits);
var expectedAudits = new[]{ audits.First(), audits.Last() };
var documentIds = expectedAudits.Select(a => a.DocumentId).ToArray();
var retrievedAudits = await _repository.GetDocumentAudits(documentIds);
retrievedAudits.Should().HaveSameCount(documentIds);
expectedAudits.Should().BeEquivalentTo(retrievedAudits, a => a.Using(DateTimeComparison.Truncated));
}
[Test]
public async Task GetDocumentAudits_ActionAuditRecordsExistForId_ReturnsMatchedLogsById()
{
var audits = EnumerableDataSeriesFactory.CreateSeries(() => ActionAuditFactory(), 5).ToArray();
await _collectionFactory.GetCollection<DocumentAudit>().InsertManyAsync(audits);
var expectedAudits = new[] { audits.First(), audits.Last() };
var documentIds = expectedAudits.Select(a => a.DocumentId).ToArray();
var retrievedAudits = await _repository.GetDocumentAudits(documentIds);
retrievedAudits.Should().HaveSameCount(documentIds);
expectedAudits.Should().BeEquivalentTo(retrievedAudits, a => a.Using(DateTimeComparison.Truncated));
}
[Test]
public async Task SaveDocumentAudits_FieldAuditsSubmitted_SavesAll()
{
var audits = EnumerableDataSeriesFactory.CreateSeries(() => FieldAuditFactory(), 5).ToArray();
await _repository.SaveDocumentAudits(audits);
var savedAudits = await (
await _collectionFactory
.GetCollection<DocumentAudit>()
.FindAsync(Builders<DocumentAudit>.Filter.In
(
nameof(DocumentAudit.DocumentId),
audits.Select(a => a.DocumentId))
)
).ToListAsync();
audits.Should().BeEquivalentTo(savedAudits, a => a.Using(DateTimeComparison.Truncated));
}
[Test]
public async Task SaveDocumentAudits_ActionAuditsSubmitted_SavesAll()
{
var audits = EnumerableDataSeriesFactory.CreateSeries(() => ActionAuditFactory(), 5).ToArray();
await _repository.SaveDocumentAudits(audits);
var savedAudits = await (
await _collectionFactory
.GetCollection<DocumentAudit>()
.FindAsync(Builders<DocumentAudit>.Filter.In(nameof(DocumentAudit.DocumentId),
audits.Select(a => a.DocumentId)))
).ToListAsync();
audits.Should().BeEquivalentTo(savedAudits, a => a.Using(DateTimeComparison.Truncated));
}
[Test]
public async Task SaveDocumentAudits_MultipleTypesOfAudits_CanRetrieveAll()
{
var documentId = Guid.NewGuid();
var fieldAudits = EnumerableDataSeriesFactory.CreateSeries<DocumentAudit>(() => FieldAuditFactory(documentId), 5);
var actionAudits = EnumerableDataSeriesFactory.CreateSeries<DocumentAudit>(() => ActionAuditFactory(documentId), 5);
var audits = fieldAudits.Concat(actionAudits).ToArray();
await _repository.SaveDocumentAudits(audits);
var savedAudits = await _repository.GetDocumentAudits(documentId);
audits.Should().BeEquivalentTo(savedAudits, a => a.Using(DateTimeComparison.Truncated));
}
private static DocumentFieldAudit FieldAuditFactory(Guid? documentId = null) => new
(
documentId ?? Guid.NewGuid(),
Guid.NewGuid().ToString(),
DateTime.UtcNow,
Guid.NewGuid().ToString(),
Guid.NewGuid().ToString(),
Guid.NewGuid().ToString(),
Guid.NewGuid().ToString()
);
private static DocumentActionAudit ActionAuditFactory(Guid? documentId = null) => new
(
documentId ?? Guid.NewGuid(),
Guid.NewGuid().ToString(),
DateTime.UtcNow,
Guid.NewGuid().ToString(),
(DocumentAction)DateTime.Now.Millisecond,
Guid.NewGuid().ToString()
);
public void Dispose()
{
_collectionFactory.Dispose();
}
}
@@ -0,0 +1,33 @@
using LCU.API.Internal.DocumentLib.Containers;
namespace LCU.API.Internal.DocumentLibTest.TestDataFactories;
public static class DocumentInformationFactory
{
public static DocumentInformation CreateDocumentInformation() => new()
{
DocumentId = Guid.NewGuid(),
DocumentBlobId = Guid.NewGuid(),
TraceId = Guid.NewGuid(),
Name = Guid.NewGuid().ToString(),
Type = Guid.NewGuid().ToString(),
Source = Guid.NewGuid().ToString(),
DataSource = Guid.NewGuid().ToString(),
FileSize = DateTime.Now.Millisecond,
ScanStatus = DateTime.Now.Millisecond % 2 == 0,
UploadDate = DateTime.UtcNow,
LastModifiedDate = DateTime.UtcNow,
CreatedDate = DateTime.UtcNow,
PersonNumbers = new[] { Guid.NewGuid().ToString(), Guid.NewGuid().ToString() },
OrganizationNumbers = new []{ Guid.NewGuid().ToString() , Guid.NewGuid().ToString() },
AccountNumber = Guid.NewGuid().ToString(),
Attributes = new Dictionary<string, string>
{
["PersonNumber"] = Guid.NewGuid().ToString(),
["PersonNumbers"] = $"{Guid.NewGuid()},{Guid.NewGuid()}",
["OrganizationNumber"] = Guid.NewGuid().ToString(),
["OrganizationNumbers"] = $"{Guid.NewGuid()},{Guid.NewGuid()}",
["AccountNumber"] = Guid.NewGuid().ToString()
},
};
}
@@ -0,0 +1,8 @@
namespace LCU.API.Internal.DocumentLibTest.TestDataFactories;
public static class EnumerableDataSeriesFactory
{
public static IEnumerable<T> CreateSeries<T>(Func<T> singleFactory, int count) => Enumerable
.Repeat(singleFactory, count)
.Select(f => f());
}
@@ -0,0 +1,19 @@
using LCU.API.Internal.DocumentLib.Utilities;
using Mongo2Go;
using MongoDB.Driver;
namespace LCU.API.Internal.DocumentLibTest.TestDoubles;
public class FakeMongoCollectionFactory : IMongoCollectionFactory, IDisposable
{
private readonly MongoDbRunner _runner = MongoDbRunner.Start();
public IMongoCollection<T> GetCollection<T>() => new MongoClient(_runner.ConnectionString)
.GetDatabase(typeof(T).Name)
.GetCollection<T>(typeof(T).Name);
public void Dispose()
{
_runner.Dispose();
}
}
@@ -0,0 +1 @@
global using NUnit.Framework;
@@ -0,0 +1,119 @@
using System;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Azure;
using Azure.Storage.Blobs;
using Azure.Storage.Blobs.Models;
using LCU.API.Internal.DocumentLib.DataAccess;
using Moq;
using NUnit.Framework;
namespace LCU.API.Internal.DocumentTest.DataAccess;
public class BlobApiDaoTest
{
private BlobApiDao _dao;
private Mock<BlobClient> _mockClient;
[SetUp]
public void Setup()
{
var mockBlobClient = new Mock<IBlobStorageClient>();
var mockBlobContainerClient = new Mock<BlobContainerClient>();
mockBlobClient.Setup(client => client.GetClient()).Returns(mockBlobContainerClient.Object);
_dao = new BlobApiDao(mockBlobClient.Object);
_mockClient = new Mock<BlobClient>();
mockBlobContainerClient.Setup(client => client.GetBlobClient(It.IsAny<string>())).Returns(_mockClient.Object);
}
[Test]
public async Task SaveDocumentToBlob_Success()
{
var fileData = Encoding.UTF8.GetBytes("test");
_mockClient.Setup( mock => mock.UploadAsync(It.IsAny<Stream>())).Verifiable();
var result = await _dao.SaveDocumentToBlob(fileData);
Assert.NotNull(result);
_mockClient.Verify();
}
[Test]
public void SaveDocumentToBlob_Exception_ThrowsException()
{
var fileData = Encoding.UTF8.GetBytes("test");
_mockClient.Setup( mock => mock.UploadAsync(It.IsAny<Stream>())).ThrowsAsync(new Exception("Exception thrown from unit test.")).Verifiable();
Assert.ThrowsAsync<Exception>(async () => await _dao.SaveDocumentToBlob(fileData));
_mockClient.Verify();
}
[Test]
public async Task UpdateDocumentInBlob_Success()
{
var blobDocId = Guid.NewGuid();
var fileData = Encoding.UTF8.GetBytes("test");
_mockClient.Setup( mock => mock.UploadAsync(It.IsAny<Stream>(), true, CancellationToken.None)).Verifiable();
await _dao.UpdateDocumentInBlob(blobDocId, fileData);
_mockClient.Verify();
}
[Test]
public void UpdateDocumentInBlob_Exception_ThrowsException()
{
var blobDocId = Guid.NewGuid();
var fileData = Encoding.UTF8.GetBytes("test");
_mockClient.Setup( mock => mock.UploadAsync(It.IsAny<Stream>(), true, CancellationToken.None)).ThrowsAsync(new Exception("Exception thrown from unit test.")).Verifiable();
Assert.ThrowsAsync<Exception>(async () => await _dao.UpdateDocumentInBlob(blobDocId, fileData));
_mockClient.Verify();
}
[Test]
public async Task RemoveDocumentFromBlob_Success()
{
var blobDocId = Guid.NewGuid();
_mockClient.Setup(mock => mock.DeleteAsync(DeleteSnapshotsOption.IncludeSnapshots, It.IsAny<BlobRequestConditions>(), CancellationToken.None)).Verifiable();
await _dao.RemoveDocumentFromBlob(blobDocId);
_mockClient.Verify();
}
[Test]
public void RemoveDocumentFromBlob_Exception_ThrowsException()
{
var blobDocId = Guid.NewGuid();
_mockClient.Setup( mock => mock.DeleteAsync(DeleteSnapshotsOption.IncludeSnapshots, It.IsAny<BlobRequestConditions>(), CancellationToken.None)).ThrowsAsync(new Exception("Exception thrown from unit test.")).Verifiable();
Assert.ThrowsAsync<Exception>(async () => await _dao.RemoveDocumentFromBlob(blobDocId));
_mockClient.Verify();
}
[Test]
public async Task GetDocumentFromBlob_Success()
{
var blobDocId = Guid.NewGuid();
_mockClient.Setup(mock => mock.DownloadStreamingAsync(It.IsAny<HttpRange>(), It.IsAny<BlobRequestConditions>(), It.IsAny<bool>(), It.IsAny<IProgress<long>>(),It.IsAny<CancellationToken>())).Verifiable();
await _dao.GetDocumentFromBlob(blobDocId);
_mockClient.Verify();
}
[Test]
public void GetDocumentFromBlob_Exception_ThrowsException()
{
var blobDocId = Guid.NewGuid();
_mockClient.Setup( mock => mock.DownloadStreamingAsync(It.IsAny<HttpRange>(), It.IsAny<BlobRequestConditions>(), It.IsAny<bool>(), It.IsAny<IProgress<long>>(), It.IsAny<CancellationToken>())).ThrowsAsync(new Exception("Exception thrown from unit test.")).Verifiable();
Assert.ThrowsAsync<Exception>(async () => await _dao.GetDocumentFromBlob(blobDocId));
_mockClient.Verify();
}
}
@@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.7.0" />
<PackageReference Include="LCU.API.Internal.NotificationsApiLib" Version="2.1.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="Moq" Version="4.17.2" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
<PackageReference Include="coverlet.collector" Version="3.1.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\LCU.API.Internal.Document\LCU.API.Internal.Document.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,175 @@
using System;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Azure;
using Azure.Storage.Blobs.Models;
using LCU.API.Internal.Document.Exceptions;
using LCU.API.Internal.Document.Services;
using LCU.API.Internal.DocumentLib.DataAccess;
using Moq;
using NUnit.Framework;
namespace LCU.API.Internal.DocumentTest.Services;
public class BlobApiServiceTest
{
private BlobApiService _blobApiService;
private Mock<IBlobApiDao> _mockBlobDao;
[SetUp]
public void Setup()
{
_mockBlobDao = new Mock<IBlobApiDao>();
_blobApiService = new BlobApiService(_mockBlobDao.Object);
}
[Test]
public async Task GetDocumentContent_Success()
{
var docBlobId = Guid.NewGuid();
await using var testStream = new MemoryStream(Encoding.UTF8.GetBytes("whatever"));
var mockContent = BlobsModelFactory.BlobDownloadStreamingResult(testStream);
var mockContentResponse = new Mock<Response<BlobDownloadStreamingResult>>();
mockContentResponse.Setup(content => content.Value).Returns(mockContent).Verifiable();
_mockBlobDao
.Setup(x => x.GetDocumentFromBlob(docBlobId)).ReturnsAsync(mockContentResponse.Object)
.Verifiable();
var returnedDocInfo = await _blobApiService.GetDocumentContent(docBlobId);
Assert.NotNull(returnedDocInfo);
_mockBlobDao.Verify();
}
[Test]
public void GetDocument_ContentValueIsNull_DocumentBlobServiceExceptionThrown()
{
var docBlobId = Guid.NewGuid();
_mockBlobDao
.Setup(x => x.GetDocumentFromBlob(docBlobId)).ReturnsAsync((Response<BlobDownloadStreamingResult>)null)
.Verifiable();
Assert.ThrowsAsync<BlobApiServiceException>(async() => await _blobApiService.GetDocumentContent(docBlobId));
_mockBlobDao.Verify();
}
[Test]
public void GetDocument_ExceptionThrown_DocumentBlobServiceExceptionThrown()
{
var docBlobId = Guid.NewGuid();
_mockBlobDao
.Setup(x => x.GetDocumentFromBlob(docBlobId)).ThrowsAsync(new Exception("Thrown from unit test"))
.Verifiable();
Assert.ThrowsAsync<BlobApiServiceException>(async() => await _blobApiService.GetDocumentContent(docBlobId));
_mockBlobDao.Verify();
}
[Test]
public async Task UploadDocumentContent_Success()
{
var docContent = new byte[10];
_mockBlobDao.Setup(x => x.SaveDocumentToBlob(docContent)).Verifiable();
await _blobApiService.UploadDocumentContent(docContent);
_mockBlobDao.Verify();
}
[Test]
public void UploadDocumentContent_Exception_ThrowsBlobApiServiceException()
{
var docContent = new byte[10];
_mockBlobDao.Setup(x => x.SaveDocumentToBlob(docContent)).ThrowsAsync(new Exception("Thrown from unit test")).Verifiable();
Assert.ThrowsAsync<BlobApiServiceException>(async() => await _blobApiService.UploadDocumentContent(docContent));
_mockBlobDao.Verify();
}
[Test]
public async Task UpdateDocumentContent_Success()
{
var docBlobId = Guid.NewGuid();
var docContent = new byte[10];
_mockBlobDao.Setup(x => x.UpdateDocumentInBlob(docBlobId, docContent)).Verifiable();
await _blobApiService.UpdateDocumentContent(docBlobId, docContent);
_mockBlobDao.Verify();
}
[Test]
public void UpdateDocumentContent_Exception_ThrowsBlobApiServiceException()
{
var docBlobId = Guid.NewGuid();
var docContent = new byte[10];
_mockBlobDao.Setup(x => x.UpdateDocumentInBlob(docBlobId, docContent)).ThrowsAsync(new Exception("Thrown from unit test")).Verifiable();
Assert.ThrowsAsync<BlobApiServiceException>(async() => await _blobApiService.UpdateDocumentContent(docBlobId, docContent));
_mockBlobDao.Verify();
}
[Test]
public void UpdateDocumentContent_Exception_DocumentBlobIdIsNull()
{
Guid? docBlobId = null;
var docContent = new byte[10];
Assert.ThrowsAsync<BlobApiServiceException>(async() => await _blobApiService.UpdateDocumentContent(docBlobId, docContent));
}
[Test]
public async Task DeleteDocumentContent_Success()
{
var docBlobId = Guid.NewGuid();
_mockBlobDao.Setup(x => x.RemoveDocumentFromBlob(docBlobId)).Verifiable();
await _blobApiService.DeleteDocumentContent(docBlobId);
_mockBlobDao.Verify();
}
[Test]
public void DeleteDocumentContent_Exception_ThrowsBlobApiServiceException()
{
var docBlobId = Guid.NewGuid();
_mockBlobDao.Setup(x => x.RemoveDocumentFromBlob(docBlobId)).ThrowsAsync(new Exception("Thrown from unit test")).Verifiable();
Assert.ThrowsAsync<BlobApiServiceException>(async() => await _blobApiService.DeleteDocumentContent(docBlobId));
_mockBlobDao.Verify();
}
[Test]
public void DeleteDocumentContent_Exception_DocumentBlobIdIsNull()
{
Guid? docBlobId = null;
Assert.ThrowsAsync<BlobApiServiceException>(async() => await _blobApiService.DeleteDocumentContent(docBlobId));
}
[Test]
public async Task GetDocument_BlobGuidIsEmpty_ReturnsEmptyContent()
{
var docBlobId = Guid.Empty;
var emptyContent = Array.Empty<byte>();
var result = await _blobApiService.GetDocumentContent(docBlobId);
Assert.AreEqual(emptyContent, result);
_mockBlobDao.Verify(s => s.GetDocumentFromBlob(It.IsAny<Guid>()), Times.Never);
}
[Test]
public async Task GetDocument_BlobGuidIsNull_ReturnsEmptyContent()
{
Guid? docBlobId = null;
var emptyContent = Array.Empty<byte>();
var result = await _blobApiService.GetDocumentContent(docBlobId);
Assert.AreEqual(emptyContent, result);
_mockBlobDao.Verify(s => s.GetDocumentFromBlob(It.IsAny<Guid>()), Times.Never);
}
}
@@ -0,0 +1,557 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using FluentAssertions;
using LCU.API.Internal.Document.Exceptions;
using LCU.API.Internal.Document.Services;
using LCU.API.Internal.DocumentLib.Containers;
using LCU.API.Internal.DocumentLib.Repositories;
using LCU.Library.Internal.DocumentModels.Requests;
using Moq;
using NUnit.Framework;
namespace LCU.API.Internal.DocumentTest.Services;
public class DocumentAuditServiceTest
{
private DocumentAuditService _documentAuditService;
private Mock<IDocumentAuditRepository> _mockDocumentAuditRepository;
[SetUp]
public void Setup()
{
_mockDocumentAuditRepository = new Mock<IDocumentAuditRepository>();
_documentAuditService = new DocumentAuditService(_mockDocumentAuditRepository.Object);
}
[Test]
public async Task GetDocumentAudits_RepositoryReturnsActionAudits_ReturnsAuditsTransformedToResponse()
{
var documentId = Guid.NewGuid();
var actionAudits = new List<DocumentAudit>
{
new DocumentActionAudit
(
documentId,
Guid.NewGuid().ToString(),
DateTime.Now,
Guid.NewGuid().ToString(),
Valid<DocumentAction>(),
Guid.NewGuid().ToString()
)
};
_mockDocumentAuditRepository
.Setup(r => r.GetDocumentAudits(documentId))
.ReturnsAsync(actionAudits);
var audits = await _documentAuditService.GetDocumentAudits(documentId);
actionAudits.Should().HaveSameCount(audits);
actionAudits.Should().AllSatisfy(a => audits.Should().ContainEquivalentOf(a));
}
[Test]
public async Task GetDocumentAudits_RepositoryReturnsFieldAudits_ReturnsAuditsTransformedToResponse()
{
var documentId = Guid.NewGuid();
var actionAudits = new List<DocumentAudit>
{
new DocumentFieldAudit
(
documentId,
Guid.NewGuid().ToString(),
DateTime.Now,
Guid.NewGuid().ToString(),
Guid.NewGuid().ToString(),
Guid.NewGuid(),
Guid.NewGuid()
)
};
_mockDocumentAuditRepository
.Setup(r => r.GetDocumentAudits(documentId))
.ReturnsAsync(actionAudits);
var audits = await _documentAuditService.GetDocumentAudits(documentId);
actionAudits.Should().HaveSameCount(audits);
actionAudits.Should().AllSatisfy(a => audits.Should().ContainEquivalentOf(a));
}
[Test]
public async Task GetDocumentAudits_RepositoryReturnsAnyAuditsOfUnknownType_ThrowsDocumentAuditServiceException()
{
var documentId = Guid.NewGuid();
var actionAudits = new List<DocumentAudit>
{
new UnknownAuditType
(
documentId,
Guid.NewGuid().ToString(),
DateTime.Now,
Guid.NewGuid().ToString()
)
};
_mockDocumentAuditRepository
.Setup(r => r.GetDocumentAudits(documentId))
.ReturnsAsync(actionAudits);
Assert.ThrowsAsync<DocumentAuditServiceException>(() => _documentAuditService.GetDocumentAudits(documentId));
}
[Test]
public void GetDocumentAudits_DocumentAuditRepositoryThrowsException_ThrowsDocumentAuditServiceExceptionThrown()
{
var docId = Guid.NewGuid();
_mockDocumentAuditRepository
.Setup(x => x.GetDocumentAudits(docId))
.ThrowsAsync(new Exception("Thrown From Unit Test"))
.Verifiable();
Assert.ThrowsAsync<DocumentAuditServiceException>(async () => await _documentAuditService.GetDocumentAudits(docId));
_mockDocumentAuditRepository.Verify();
}
[Test]
public void GetDocumentAudits_Exception_DocumentAuditServiceExceptionThrown()
{
var docId = Guid.NewGuid();
_mockDocumentAuditRepository
.Setup(x => x.GetDocumentAudits(docId))
.ThrowsAsync(new Exception("Thrown From Unit Test"))
.Verifiable();
Assert.ThrowsAsync<DocumentAuditServiceException>(async () => await _documentAuditService.GetDocumentAudits(docId));
_mockDocumentAuditRepository.Verify();
}
[Test]
public async Task CreateDocumentUploadedAudit_Success()
{
var uploadDate = DateTime.Parse("2022-04-28T17:32:35.093Z");
var documentInformation = GenerateDocumentInformation();
var audits = new DocumentAudit[]
{
new DocumentActionAudit
(
documentInformation.DocumentId,
"System",
uploadDate,
"System",
DocumentAction.Upload,
"Document Uploaded"
)
};
_mockDocumentAuditRepository
.Setup(x => x.SaveDocumentAudits(It.Is<DocumentAudit[]>(list =>
list.Length == 1
&& list.All(a => a is DocumentActionAudit)
&& ((DocumentActionAudit)list.FirstOrDefault()!).Action == DocumentAction.Upload
&& ((DocumentActionAudit)list.FirstOrDefault()!).UserId == "System"
&& ((DocumentActionAudit)list.FirstOrDefault()!).Source == "System"
&& ((DocumentActionAudit)list.FirstOrDefault()!).ActionDescription == "Document Uploaded")))
.ReturnsAsync(audits)
.Verifiable();
var result = await _documentAuditService.CreateDocumentUploadedAudit(documentInformation.DocumentId, uploadDate);
var resultArray = result.ToArray();
Assert.NotNull(result);
audits.Should().HaveSameCount(resultArray);
audits.Should().AllSatisfy(a => resultArray.Should().ContainEquivalentOf(a));
_mockDocumentAuditRepository.Verify();
}
[Test]
public void CreateDocumentUploadedAudit_DocumentAuditRepositoryThrowsException_ThrowsNewDocumentAuditServiceExceptionThrown()
{
var uploadDate = DateTime.Parse("2022-04-28T17:32:35.093Z");
var documentInformation = GenerateDocumentInformation();
_mockDocumentAuditRepository
.Setup(x => x.SaveDocumentAudits(It.Is<DocumentAudit[]>(list => list.Any())))
.ThrowsAsync(new DocumentAuditServiceException("Thrown From Unit Test"))
.Verifiable();
Assert.ThrowsAsync<DocumentAuditServiceException>(() => _documentAuditService.CreateDocumentUploadedAudit(documentInformation.DocumentId, uploadDate));
_mockDocumentAuditRepository.Verify();
}
[Test]
public void CreateDocumentUploadedAudit_Exception_DocumentAuditServiceExceptionThrown()
{
var uploadDate = DateTime.Parse("2022-04-28T17:32:35.093Z");
var documentInformation = GenerateDocumentInformation();
_mockDocumentAuditRepository
.Setup(x => x.SaveDocumentAudits(It.Is<DocumentAudit[]>(list => list.Any())))
.ThrowsAsync(new Exception("Thrown From Unit Test"))
.Verifiable();
Assert.ThrowsAsync<DocumentAuditServiceException>(() => _documentAuditService.CreateDocumentUploadedAudit(documentInformation.DocumentId, uploadDate));
_mockDocumentAuditRepository.Verify();
}
[Test]
public async Task CreateDocumentDeletedAudit_Success()
{
var deleteDate = DateTime.Parse("2022-04-28T17:32:35.093Z");
var documentInformation = GenerateDocumentInformation();
var audits = new DocumentAudit[]
{
new DocumentActionAudit
(
documentInformation.DocumentId,
"System",
deleteDate,
"System",
DocumentAction.Delete,
"Document Deleted"
)
};
_mockDocumentAuditRepository
.Setup(x => x.SaveDocumentAudits(It.Is<DocumentAudit[]>(list =>
list.Length == 1
&& list.All(a => a is DocumentActionAudit)
&& ((DocumentActionAudit)list.FirstOrDefault()!).Action == DocumentAction.Delete
&& ((DocumentActionAudit)list.FirstOrDefault()!).UserId == "System"
&& ((DocumentActionAudit)list.FirstOrDefault()!).Source == "System"
&& ((DocumentActionAudit)list.FirstOrDefault()!).ActionDescription == "Document Deleted")))
.ReturnsAsync(audits)
.Verifiable();
var result = await _documentAuditService.CreateDocumentDeletedAudit(documentInformation.DocumentId, deleteDate);
var resultArray = result.ToArray();
Assert.NotNull(result);
audits.Should().HaveSameCount(resultArray);
audits.Should().AllSatisfy(a => resultArray.Should().ContainEquivalentOf(a));
_mockDocumentAuditRepository.Verify();
}
[Test]
public void CreateDocumentDeletedAudit_DocumentAuditRepositoryThrowsException_ThrowsNewDocumentAuditServiceExceptionThrown()
{
var deleteDate = DateTime.Parse("2022-04-28T17:32:35.093Z");
var documentInformation = GenerateDocumentInformation();
_mockDocumentAuditRepository
.Setup(x => x.SaveDocumentAudits(It.Is<DocumentAudit[]>(list => list.Any())))
.ThrowsAsync(new DocumentAuditServiceException("Thrown From Unit Test"))
.Verifiable();
Assert.ThrowsAsync<DocumentAuditServiceException>(() => _documentAuditService.CreateDocumentDeletedAudit(documentInformation.DocumentId, deleteDate));
_mockDocumentAuditRepository.Verify();
}
[Test]
public void CreateDocumentDeletedAudit_Exception_DocumentAuditServiceExceptionThrown()
{
var deleteDate = DateTime.Parse("2022-04-28T17:32:35.093Z");
var documentInformation = GenerateDocumentInformation();
_mockDocumentAuditRepository
.Setup(x => x.SaveDocumentAudits(It.Is<DocumentAudit[]>(list => list.Any())))
.ThrowsAsync(new Exception("Thrown From Unit Test"))
.Verifiable();
Assert.ThrowsAsync<DocumentAuditServiceException>(() => _documentAuditService.CreateDocumentDeletedAudit(documentInformation.DocumentId, deleteDate));
_mockDocumentAuditRepository.Verify();
}
[Test]
public async Task CreateDocumentContentDeletedAudit_Success()
{
var deleteDate = DateTime.Parse("2022-04-28T17:32:35.093Z");
var documentInformation = GenerateDocumentInformation();
var reason = Guid.NewGuid().ToString();
var reasonFull = $"DocumentContent Deleted. Reason: {reason}";
var audits = new DocumentAudit[]
{
new DocumentActionAudit
(
documentInformation.DocumentId,
"System",
deleteDate,
"System",
DocumentAction.DeleteContent,
reasonFull
)
};
_mockDocumentAuditRepository
.Setup(x => x.SaveDocumentAudits(It.Is<DocumentAudit[]>(list =>
list.Length == 1
&& list.All(a => a is DocumentActionAudit)
&& ((DocumentActionAudit)list.FirstOrDefault()!).Action == DocumentAction.DeleteContent
&& ((DocumentActionAudit)list.FirstOrDefault()!).UserId == "System"
&& ((DocumentActionAudit)list.FirstOrDefault()!).Source == "System"
&& ((DocumentActionAudit)list.FirstOrDefault()!).ActionDescription == reasonFull)))
.ReturnsAsync(audits)
.Verifiable();
var result = await _documentAuditService.CreateDocumentContentDeletedAudit(documentInformation.DocumentId, deleteDate, reason);
var resultArray = result.ToArray();
Assert.NotNull(result);
audits.Should().HaveSameCount(resultArray);
audits.Should().AllSatisfy(a => resultArray.Should().ContainEquivalentOf(a));
_mockDocumentAuditRepository.Verify();
}
[Test]
public void CreateDocumentContentDeletedAudit_DocumentAuditRepositoryThrowsException_ThrowsNewDocumentAuditServiceExceptionThrown()
{
var deleteDate = DateTime.Parse("2022-04-28T17:32:35.093Z");
var documentInformation = GenerateDocumentInformation();
var reason = Guid.NewGuid().ToString();
_mockDocumentAuditRepository
.Setup(x => x.SaveDocumentAudits(It.Is<DocumentAudit[]>(list => list.Any())))
.ThrowsAsync(new DocumentAuditServiceException("Thrown From Unit Test"))
.Verifiable();
Assert.ThrowsAsync<DocumentAuditServiceException>(() => _documentAuditService.CreateDocumentContentDeletedAudit(documentInformation.DocumentId, deleteDate, reason));
_mockDocumentAuditRepository.Verify();
}
[Test]
public void CreateDocumentContentDeletedAudit_Exception_DocumentAuditServiceExceptionThrown()
{
var deleteDate = DateTime.Parse("2022-04-28T17:32:35.093Z");
var documentInformation = GenerateDocumentInformation();
var reason = Guid.NewGuid().ToString();
_mockDocumentAuditRepository
.Setup(x => x.SaveDocumentAudits(It.Is<DocumentAudit[]>(list => list.Any())))
.ThrowsAsync(new Exception("Thrown From Unit Test"))
.Verifiable();
Assert.ThrowsAsync<DocumentAuditServiceException>(() => _documentAuditService.CreateDocumentContentDeletedAudit(documentInformation.DocumentId, deleteDate, reason));
_mockDocumentAuditRepository.Verify();
}
[Test]
public async Task CreateDocumentAudits_No_Differences_Success()
{
var updateDate = DateTime.Parse("2022-04-28T17:32:35.093Z");
var docInformationOld = GenerateDocumentInformation();
var docInformationNew = GenerateDocumentInformation();
var result = await _documentAuditService.CreateDocumentAudits(docInformationOld, docInformationNew, false, updateDate, "testUser");
Assert.NotNull(result);
Assert.IsEmpty(result);
_mockDocumentAuditRepository.Verify(x => x.SaveDocumentAudits(It.IsAny<DocumentAudit[]>()), Times.Never);
}
[Test]
public async Task CreateDocumentAudits_No_Differences_ContentChange_Success()
{
var updateDate = DateTime.Parse("2022-04-28T17:32:35.093Z");
var userId = Guid.NewGuid().ToString();
var docInformationOld = GenerateDocumentInformation();
var docInformationNew = GenerateDocumentInformation();
_mockDocumentAuditRepository
.Setup(x => x.SaveDocumentAudits(It.Is<DocumentAudit[]>(list =>
list.Length == 1
&& list.All(a => a is DocumentFieldAudit)
&& ((DocumentFieldAudit)list.FirstOrDefault()!).Field == "Content"
&& list.All(audit => string.Equals(userId, ((DocumentFieldAudit)audit).UserId)))))
.ReturnsAsync(new List<DocumentAudit>())
.Verifiable();
await _documentAuditService.CreateDocumentAudits(docInformationOld, docInformationNew, true, updateDate, userId);
_mockDocumentAuditRepository.Verify();
}
[Test]
public async Task CreateDocumentAudits_All_Differences_Success()
{
var updateDate = DateTime.Parse("2022-04-28T17:32:35.093Z");
var docInformationOld = GenerateDocumentInformation();
var docInformationEverythingChanges = GenerateDocumentInformation();
var userId = Guid.NewGuid().ToString();
docInformationEverythingChanges.Name = "BrandNewName";
docInformationEverythingChanges.Type = "BrandNewType";
docInformationEverythingChanges.ScanStatus = true;
docInformationEverythingChanges.DataSource = "NEW SOURCE";
docInformationEverythingChanges.Attributes = new Dictionary<string, string>
{
{ "NEW_FIELD", "NEW_VALUE" }
};
_mockDocumentAuditRepository
.Setup(x => x.SaveDocumentAudits(It.Is<DocumentAudit[]>(list =>
list.Length == 4
&& list.All(a => a is DocumentFieldAudit)
&& list.Any(audit => string.Equals("Name", ((DocumentFieldAudit)audit).Field))
&& list.Any(audit => string.Equals("Type", ((DocumentFieldAudit)audit).Field))
&& list.Any(audit => string.Equals("ScanStatus", ((DocumentFieldAudit)audit).Field))
&& list.Any(audit => string.Equals("Attributes", ((DocumentFieldAudit)audit).Field))
&& list.All(audit => string.Equals(userId, ((DocumentFieldAudit)audit).UserId)))))
.ReturnsAsync(new List<DocumentAudit>())
.Verifiable();
await _documentAuditService.CreateDocumentAudits(docInformationOld, docInformationEverythingChanges, false, updateDate, userId);
_mockDocumentAuditRepository.Verify();
}
[Test]
public void CreateDocumentAudits_DocumentAuditRepositoryThrowsException_ThrowsNewDocumentAuditServiceExceptionThrown()
{
var updateDate = DateTime.Parse("2022-04-28T17:32:35.093Z");
var docInformationOld = GenerateDocumentInformation();
var docInformationNew = GenerateDocumentInformation();
var userId = Guid.NewGuid().ToString();
docInformationNew.Name = "NewName.txt";
_mockDocumentAuditRepository
.Setup(x => x.SaveDocumentAudits(It.Is<DocumentAudit[]>(list => list.Any())))
.ThrowsAsync(new DocumentAuditServiceException("Thrown From Unit Test"))
.Verifiable();
Assert.ThrowsAsync<DocumentAuditServiceException>(() => _documentAuditService.CreateDocumentAudits(docInformationOld, docInformationNew, false, updateDate, userId));
_mockDocumentAuditRepository.Verify();
}
[Test]
public void CreateDocumentAudits_Exception_DocumentAuditServiceExceptionThrown()
{
var updateDate = DateTime.Parse("2022-04-28T17:32:35.093Z");
var docInformationOld = GenerateDocumentInformation();
var docInformationNew = GenerateDocumentInformation();
var userId = Guid.NewGuid().ToString();
docInformationNew.Name = "NewName.txt";
_mockDocumentAuditRepository
.Setup(x => x.SaveDocumentAudits(It.Is<DocumentAudit[]>(list => list.Any())))
.ThrowsAsync(new Exception("Thrown From Unit Test"))
.Verifiable();
Assert.ThrowsAsync<DocumentAuditServiceException>(() => _documentAuditService.CreateDocumentAudits(docInformationOld, docInformationNew, false, updateDate, userId));
_mockDocumentAuditRepository.Verify();
}
[Test]
public void AuditAction_RepositoryThrowsException_ThrowsDocumentAuditServiceException()
{
var documentId = Guid.NewGuid();
var userId = Guid.NewGuid().ToString();
var source = Guid.NewGuid().ToString();
var action = Valid<DocumentAction>();
_mockDocumentAuditRepository
.Setup(r => r.SaveDocumentAudits(It.Is<DocumentActionAudit>(a =>
a.DocumentId == documentId
&& a.UserId == userId
&& a.Source == source
&& a.Action == action)))
.ThrowsAsync(new Exception());
Assert.ThrowsAsync<DocumentAuditServiceException>(() => _documentAuditService.AuditAction(documentId, userId, source, action));
}
[Test]
public void AuditAction_UnknownActionType_ThrowsDocumentAuditServiceException()
{
var documentId = Guid.NewGuid();
var userId = Guid.NewGuid().ToString();
var source = Guid.NewGuid().ToString();
var action = Invalid<DocumentAction>();
Assert.ThrowsAsync<DocumentAuditServiceException>(() => _documentAuditService.AuditAction(documentId, userId, source, action));
}
private static DocumentAction[] AllDocumentActions => AllValid<DocumentAction>();
[TestCaseSource(nameof(AllDocumentActions))]
public async Task AuditAction_ValidAction_SavesActionAudit(DocumentAction action)
{
var documentId = Guid.NewGuid();
var userId = Guid.NewGuid().ToString();
var source = Guid.NewGuid().ToString();
_mockDocumentAuditRepository
.Setup(r => r.SaveDocumentAudits(It.Is<DocumentActionAudit>(a =>
a.DocumentId == documentId
&& a.UserId == userId
&& a.Source == source
&& a.Action == action)))
.Verifiable();
await _documentAuditService.AuditAction(documentId, userId, source, action);
_mockDocumentAuditRepository.Verify();
}
private static List<DocumentAudit> GenerateDocumentAudits()
{
return new List<DocumentAudit>
{
new DocumentFieldAudit(Guid.NewGuid(), "UnitTest", DateTime.Parse("2022-04-28T17:32:35.093Z"), "Test", "Test", "0", "1")
};
}
private static DocumentInformation GenerateDocumentInformation()
{
return new DocumentInformation
{
DocumentId = Guid.NewGuid(),
Name = "TestFile.txt",
Type = "Test",
Source = "Developer's Laptop",
ScanStatus = null,
UploadDate = DateTime.Parse("2022-03-25T19:16:12.3665642Z"),
LastModifiedDate = DateTime.Parse("0001-01-01T00:00:00"),
Attributes = new Dictionary<string, string>
{
{ "IsTest", "true" },
{ "Owner", "Me Myself" }
}
};
}
private static T[] AllValid<T>()
where T : struct, Enum
{
return Enum.GetValues<T>();
}
private static T Valid<T>()
where T : struct, Enum
{
var values = Enum.GetValues<T>();
return values[DateTime.Now.Millisecond % values.Length];
}
private static T Invalid<T>()
where T : struct, Enum
{
return (T)Convert.ChangeType(Enum.GetValues<T>().Length + 1, typeof(int));
}
private class UnknownAuditType : DocumentAudit
{
public UnknownAuditType(Guid documentId, string source, DateTime date, string userId)
: base(documentId, source, date, userId)
{
}
}
}
@@ -0,0 +1,455 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using LCU.API.Internal.Document.Configurations;
using LCU.API.Internal.Document.Exceptions;
using LCU.API.Internal.Document.Services;
using LCU.API.Internal.DocumentLib.Containers;
using LCU.API.Internal.DocumentLib.Repositories;
using LCU.Library.Internal.DocumentModels.Requests.Search;
using Moq;
using NUnit.Framework;
namespace LCU.API.Internal.DocumentTest.Services;
public class DocumentInformationServiceTest
{
private DocumentInformationService _documentInformationService;
private Mock<IDocumentInformationRepository> _mockDocumentRepository;
private ApiConfiguration _apiConfiguration;
[SetUp]
public void Setup()
{
_mockDocumentRepository = new Mock<IDocumentInformationRepository>();
_apiConfiguration = new ApiConfiguration() { FileUploadCutoffHours = -2 };
_documentInformationService = new DocumentInformationService(_mockDocumentRepository.Object,
_apiConfiguration);
}
[Test]
public async Task GetDocumentInformation_Success()
{
var docId = Guid.NewGuid();
var docBlobId = Guid.NewGuid();
var docInformation = GenerateDocumentInformation();
docInformation.DocumentBlobId = docBlobId;
docInformation.UploadDate = DateTime.UtcNow.AddDays(-1);
docInformation.ScanStatus = true;
_mockDocumentRepository
.Setup(x => x.GetDocumentInformation(docId)).ReturnsAsync(docInformation)
.Verifiable();
var returnedDocInfo = await _documentInformationService.GetDocumentInformation(docId);
Assert.NotNull(returnedDocInfo);
Assert.AreEqual(docInformation.DocumentId, returnedDocInfo?.DocumentId);
Assert.AreEqual(docInformation.Name, returnedDocInfo?.Name);
Assert.AreEqual(docInformation.Type, returnedDocInfo?.Type);
Assert.AreEqual(docInformation.Source, returnedDocInfo?.Source);
Assert.AreEqual(docInformation.ScanStatus, returnedDocInfo?.ScanStatus);
Assert.AreEqual(docInformation.UploadDate, returnedDocInfo?.UploadDate);
Assert.AreEqual(docInformation.LastModifiedDate, returnedDocInfo?.LastModifiedDate);
Assert.AreEqual(docInformation.PersonNumbers, returnedDocInfo?.PersonNumbers);
Assert.AreEqual(docInformation.OrganizationNumbers, returnedDocInfo?.OrganizationNumbers);
Assert.AreEqual(docInformation.AccountNumber, returnedDocInfo?.AccountNumber);
Assert.AreEqual(docInformation.Attributes, returnedDocInfo?.Attributes);
_mockDocumentRepository.Verify();
}
[Test]
public async Task GetDocument_NoInformationPresent_Null()
{
var docId = Guid.NewGuid();
_mockDocumentRepository
.Setup(x => x.GetDocumentInformation(docId)).ReturnsAsync(null as DocumentInformation)
.Verifiable();
var result = await _documentInformationService.GetDocumentInformation(docId);
Assert.Null(result);
_mockDocumentRepository.Verify();
}
[Test]
public async Task GetDocument_ScanStatusIsFalse_DocumentIsNull()
{
var docId = Guid.NewGuid();
var docInformation = GenerateDocumentInformation();
docInformation.ScanStatus = false;
_mockDocumentRepository
.Setup(x => x.GetDocumentInformation(docId)).ReturnsAsync(docInformation)
.Verifiable();
var result = await _documentInformationService.GetDocumentInformation(docId);
Assert.IsNull(result);
_mockDocumentRepository.Verify();
}
[Test]
public void GetDocument_ExceptionThrown_ThrowDocumentInformationServiceException()
{
var docId = Guid.NewGuid();
_mockDocumentRepository
.Setup(x => x.GetDocumentInformation(docId))
.ThrowsAsync(new Exception("Thrown from unit test"))
.Verifiable();
Assert.ThrowsAsync<DocumentInformationServiceException>(async () => await _documentInformationService.GetDocumentInformation(docId));
_mockDocumentRepository.Verify();
}
[Test]
public async Task GetDocumentInformationByBlobId_Success()
{
var docBlobId = Guid.NewGuid();
var docInformation = GenerateDocumentInformation();
docInformation.DocumentBlobId = docBlobId;
docInformation.ScanStatus = true;
_mockDocumentRepository
.Setup(x => x.GetDocumentInformationByBlobId(docBlobId)).ReturnsAsync(docInformation)
.Verifiable();
var returnedDocInfo = await _documentInformationService.GetDocumentInformationByBlobId(docBlobId);
Assert.NotNull(returnedDocInfo);
Assert.AreEqual(docInformation.DocumentId, returnedDocInfo?.DocumentId);
Assert.AreEqual(docInformation.Name, returnedDocInfo?.Name);
Assert.AreEqual(docInformation.Type, returnedDocInfo?.Type);
Assert.AreEqual(docInformation.Source, returnedDocInfo?.Source);
Assert.AreEqual(docInformation.ScanStatus, returnedDocInfo?.ScanStatus);
Assert.AreEqual(docInformation.UploadDate, returnedDocInfo?.UploadDate);
Assert.AreEqual(docInformation.LastModifiedDate, returnedDocInfo?.LastModifiedDate);
Assert.AreEqual(docInformation.PersonNumbers, returnedDocInfo?.PersonNumbers);
Assert.AreEqual(docInformation.OrganizationNumbers, returnedDocInfo?.OrganizationNumbers);
Assert.AreEqual(docInformation.AccountNumber, returnedDocInfo?.AccountNumber);
Assert.AreEqual(docInformation.Attributes, returnedDocInfo?.Attributes);
_mockDocumentRepository.Verify();
}
[Test]
public async Task GetDocumentByBlobId_NoInformationPresent_Null()
{
var docBlobId = Guid.NewGuid();
_mockDocumentRepository
.Setup(x => x.GetDocumentInformationByBlobId(docBlobId)).ReturnsAsync(null as DocumentInformation)
.Verifiable();
var result = await _documentInformationService.GetDocumentInformationByBlobId(docBlobId);
Assert.Null(result);
_mockDocumentRepository.Verify();
}
[Test]
public void GetDocumentByBlobId_ExceptionThrown_ThrowDocumentInformationServiceException()
{
var docBlobId = Guid.NewGuid();
_mockDocumentRepository
.Setup(x => x.GetDocumentInformationByBlobId(docBlobId))
.ThrowsAsync(new Exception("Thrown from unit test"))
.Verifiable();
Assert.ThrowsAsync<DocumentInformationServiceException>(async () => await _documentInformationService.GetDocumentInformationByBlobId(docBlobId));
_mockDocumentRepository.Verify();
}
[Test]
public async Task DeleteDocument_Success()
{
var docId = Guid.NewGuid();
var docBlobId = Guid.NewGuid();
var docInformation = GenerateDocumentInformation();
docInformation.DocumentBlobId = docBlobId;
_mockDocumentRepository
.Setup(x => x.GetDocumentInformation(docId)).ReturnsAsync(docInformation)
.Verifiable();
_mockDocumentRepository
.Setup(x => x.RemoveDocumentInformation(docId)).ReturnsAsync(docInformation)
.Verifiable();
await _documentInformationService.DeleteDocumentInformation(docId);
_mockDocumentRepository.Verify();
}
[Test]
public async Task DeleteDocument_NoInformationPresent_Null()
{
var docId = Guid.NewGuid();
var docBlobId = Guid.NewGuid();
var docInformation = GenerateDocumentInformation();
docInformation.DocumentBlobId = docBlobId;
_mockDocumentRepository
.Setup(x => x.GetDocumentInformation(docId)).ReturnsAsync(null as DocumentInformation)
.Verifiable();
await _documentInformationService.DeleteDocumentInformation(docId);
_mockDocumentRepository.Verify(x => x.RemoveDocumentInformation(It.IsAny<Guid>()), Times.Never);
_mockDocumentRepository.Verify();
}
[Test]
public void DeleteDocument_RemoveDocumentInfoThrowsException_ThrowDocumentInformationServiceException()
{
var docId = Guid.NewGuid();
var docBlobId = Guid.NewGuid();
var docInformation = GenerateDocumentInformation();
docInformation.DocumentBlobId = docBlobId;
_mockDocumentRepository
.Setup(x => x.GetDocumentInformation(docId)).ReturnsAsync(docInformation)
.Verifiable();
_mockDocumentRepository.Setup(x => x.RemoveDocumentInformation(docId))
.ThrowsAsync(new Exception("thrown from unit test")).Verifiable();
Assert.ThrowsAsync<DocumentInformationServiceException>(async () => await _documentInformationService.DeleteDocumentInformation(docId));
_mockDocumentRepository.Verify();
}
[Test]
public async Task UploadDocument_Success()
{
var docBlobId = Guid.NewGuid();
var docInformation = GenerateDocumentInformation();
docInformation.DocumentBlobId = docBlobId;
_mockDocumentRepository
.Setup(x => x.SaveDocumentInformation(It.Is<DocumentInformation>(info =>
info.DocumentBlobId == docBlobId)))
.ReturnsAsync(docInformation)
.Verifiable();
var result = await _documentInformationService.UploadDocumentInformation(docInformation);
Assert.AreEqual(docInformation, result);
_mockDocumentRepository.Verify();
}
[Test]
public void UploadDocument_ExceptionThrown_ThrowDocumentInformation()
{
var docInformation = GenerateDocumentInformation();
_mockDocumentRepository.Setup(x => x.SaveDocumentInformation(It.IsAny<DocumentInformation>()))
.ThrowsAsync(new Exception("Error thrown from unit test.")).Verifiable();
Assert.ThrowsAsync<DocumentInformationServiceException>(async () => await _documentInformationService.UploadDocumentInformation(docInformation));
_mockDocumentRepository.Verify();
}
[Test]
public async Task UpdateDocument_Success()
{
var docId = Guid.NewGuid();
var docInformation = GenerateDocumentInformation();
docInformation.DocumentId = docId;
_mockDocumentRepository.Setup(x => x.GetDocumentInformation(docId)).ReturnsAsync(docInformation).Verifiable();
_mockDocumentRepository
.Setup(x => x.UpdateDocumentInformation(docId, It.Is<DocumentInformation>(info =>
info.DocumentId == docId))).ReturnsAsync(docInformation)
.Verifiable();
await _documentInformationService.UpdateDocumentInformation(docId, docInformation);
_mockDocumentRepository.Verify();
}
[Test]
public async Task UpdateDocument_NoInformation_Null()
{
var docId = Guid.NewGuid();
var docInformation = GenerateDocumentInformation();
_mockDocumentRepository
.Setup(x => x.GetDocumentInformation(docId)).ReturnsAsync(null as DocumentInformation)
.Verifiable();
var result = await _documentInformationService.UpdateDocumentInformation(docId, docInformation);
Assert.Null(result);
_mockDocumentRepository.Verify();
}
[Test]
public void UpdateDocument_ExceptionThrown_ThrowDocumentInformationServiceException()
{
var docBlobId = Guid.NewGuid();
var docId = Guid.NewGuid();
var docInformation = GenerateDocumentInformation();
docInformation.DocumentId = docId;
docInformation.DocumentBlobId = docBlobId;
_mockDocumentRepository.Setup(x => x.GetDocumentInformation(docId)).ThrowsAsync(new Exception("Thrown from unit test")).Verifiable();
Assert.ThrowsAsync<DocumentInformationServiceException>(async () => await _documentInformationService.UpdateDocumentInformation(docId, docInformation));
_mockDocumentRepository.Verify(
x => x.UpdateDocumentInformation(It.IsAny<Guid>(), It.IsAny<DocumentInformation>()), Times.Never);
_mockDocumentRepository.Verify();
}
[Test]
public async Task SearchDocumentInformation_Success()
{
var searchFilter = GenerateDocumentInformationFilter();
var random = new Random(DateTime.Now.Millisecond);
var skip = random.Next();
var take = random.Next();
var docInformation = new List<DocumentInformation> { GenerateDocumentInformation() };
_mockDocumentRepository
.Setup(x => x.SearchDocumentInformation(searchFilter, skip, take, null))
.ReturnsAsync(docInformation);
var result = await _documentInformationService.SearchDocumentInformation(searchFilter, skip, take);
Assert.AreEqual(docInformation, result);
}
[Test]
public async Task SearchDocumentInformation_NoneFound_EmptyList()
{
var searchFilter = GenerateDocumentInformationFilter();
_mockDocumentRepository
.Setup(x => x.SearchDocumentInformation(searchFilter, It.IsAny<int?>(), It.IsAny<int?>(), null))
.ReturnsAsync((List<DocumentInformation>)null)
.Verifiable();
var result = await _documentInformationService.SearchDocumentInformation(searchFilter);
_mockDocumentRepository.Verify();
Assert.IsEmpty(result);
}
[Test]
public void SearchDocumentInformation_ThrowsException_DocumentInformationServiceException()
{
var searchFilter = GenerateDocumentInformationFilter();
_mockDocumentRepository
.Setup(x => x.SearchDocumentInformation(searchFilter, It.IsAny<int?>(), It.IsAny<int?>(), null))
.ThrowsAsync(new Exception("Thrown from unit test"))
.Verifiable();
Assert.ThrowsAsync<DocumentInformationServiceException>(async() => await _documentInformationService.SearchDocumentInformation(searchFilter));
_mockDocumentRepository.Verify();
}
[Test]
public void CountDocuments_RepositoryCountThrows_ThrowsDocumentInformationServiceException()
{
var filter = new DocumentInformationFilter();
_mockDocumentRepository
.Setup(r => r.CountDocuments(filter))
.ThrowsAsync(new Exception());
Assert.ThrowsAsync<DocumentInformationServiceException>(() => _documentInformationService.CountDocuments(filter));
}
[Test]
public async Task CountDocuments_RepositoryReturnsCount_ReturnsCount()
{
var filter = new DocumentInformationFilter();
var expectedCount = DateTime.Now.Millisecond;
_mockDocumentRepository
.Setup(r => r.CountDocuments(filter))
.ReturnsAsync(expectedCount);
var count = await _documentInformationService.CountDocuments(filter);
Assert.AreEqual(expectedCount, count);
}
[Test]
public async Task GetDocumentsForCleanup_Success()
{
var documents = new List<Guid> { Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid() };
_mockDocumentRepository.Setup(x => x.GetDocumentsForCleanup(It.IsAny<GetDocumentCleanupRequest>()))
.ReturnsAsync(documents).Verifiable();
var result = await _documentInformationService.GetDocumentsForCleanup(It.IsAny<GetDocumentCleanupRequest>());
Assert.NotNull(result);
Assert.IsNotEmpty(result);
Assert.AreSame(documents, result);
_mockDocumentRepository.Verify(x => x.GetDocumentsForCleanup(It.IsAny<GetDocumentCleanupRequest>()), Times.Once);
}
[Test]
public async Task GetDocumentsForCleanup_Success_NoDocumentsFound()
{
var documents = new List<Guid>();
_mockDocumentRepository.Setup(x => x.GetDocumentsForCleanup(It.IsAny<GetDocumentCleanupRequest>()))
.ReturnsAsync(documents).Verifiable();
var result = await _documentInformationService.GetDocumentsForCleanup(It.IsAny<GetDocumentCleanupRequest>());
Assert.NotNull(result);
Assert.IsEmpty(result);
Assert.AreSame(documents, result);
_mockDocumentRepository.Verify(x => x.GetDocumentsForCleanup(It.IsAny<GetDocumentCleanupRequest>()), Times.Once);
}
[Test]
public void GetDocumentsForCleanup_ThrowsDocumentInformationServiceException()
{
_mockDocumentRepository
.Setup(r => r.GetDocumentsForCleanup(It.IsAny<GetDocumentCleanupRequest>()))
.ThrowsAsync(new Exception());
Assert.ThrowsAsync<DocumentInformationServiceException>(() =>
_documentInformationService.GetDocumentsForCleanup(It.IsAny<GetDocumentCleanupRequest>()));
}
private static DocumentInformationFilter GenerateDocumentInformationFilter()
{
return new DocumentInformationFilter
{
Name = "TestSearch"
};
}
private static DocumentInformation GenerateDocumentInformation()
{
return new DocumentInformation
{
DocumentId = Guid.NewGuid(),
Name = "TestFile.txt",
Type = "Test",
Source = "Developer's Laptop",
DataSource = "DNA",
ScanStatus = null,
UploadDate = DateTime.Parse("2022-03-25T19:16:12.3665642Z"),
LastModifiedDate = DateTime.Parse("0001-01-01T00:00:00"),
PersonNumbers = new string[] { "1", "2" },
OrganizationNumbers = new string[] { "3", "4" },
AccountNumber = "5",
Attributes = new Dictionary<string, string>
{
{ "IsTest", "true" },
{ "Owner", "Me Myself" }
}
};
}
}
@@ -0,0 +1,56 @@
using System.Threading.Tasks;
using System;
using Moq;
using NUnit.Framework;
using LCU.Library.Internal.Exceptions;
using LCU.API.Internal.Document.Services;
using LCU.API.Internal.DocumentLib.DataAccess;
using LCU.API.Internal.NotificationsApiLib.Models;
using System.Collections.Generic;
namespace LCU.API.Internal.DocumentTest.Services;
public class NotificationApiServiceTest
{
private NotificationApiService _notificationApiService;
private Mock<INotificationApiDao> _mockNotificationApiDao;
[SetUp]
public void Setup()
{
_mockNotificationApiDao = new Mock<INotificationApiDao>();
_notificationApiService = new NotificationApiService(_mockNotificationApiDao.Object);
}
[Test]
public void SendEmailAsync_Exception()
{
_mockNotificationApiDao.Setup(x => x.SendEmailAsync(It.IsAny<EmailRequest>()))
.ThrowsAsync(new Exception("Error"));
var request = new EmailRequest()
{
Subject = "",
Body = "",
Recipient = new List<string>(new string[] {""})
};
Assert.ThrowsAsync<ServiceException>(async () => await _notificationApiService.SendEmailAsync(request));
_mockNotificationApiDao.Verify(x => x.SendEmailAsync(It.IsAny<EmailRequest>()), Times.Once);
}
[Test]
public async Task SendEmailAsync_Return()
{
_mockNotificationApiDao.Setup(x => x.SendEmailAsync(It.IsAny<EmailRequest>()));
var request = new EmailRequest()
{
Subject = "",
Body = "",
Recipient = new List<string>(new string[] { "" })
};
await _notificationApiService.SendEmailAsync(request);
_mockNotificationApiDao.Verify(x => x.SendEmailAsync(It.IsAny<EmailRequest>()), Times.Once);
}
}
@@ -0,0 +1,25 @@
using System;
using System.Globalization;
using System.Numerics;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace LCU.Library.Internal.DocumentModels.Converter;
public class BigIntegerConverter : JsonConverter<BigInteger>
{
public override BigInteger Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.Number)
throw new JsonException($"Found token {reader.TokenType} but expected token {JsonTokenType.Number}");
using var doc = JsonDocument.ParseValue(ref reader);
return BigInteger.Parse(doc.RootElement.GetRawText(), NumberFormatInfo.InvariantInfo);
}
public override void Write(Utf8JsonWriter writer, BigInteger value, JsonSerializerOptions options)
{
var s = value.ToString(NumberFormatInfo.InvariantInfo);
using var doc = JsonDocument.Parse(s);
doc.WriteTo(writer);
}
}
@@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using System.Dynamic;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace LCU.Library.Internal.DocumentModels.Converter;
public class DynamicObjectJsonConverter : Newtonsoft.Json.JsonConverter
{
public override void WriteJson(JsonWriter writer, dynamic value, JsonSerializer serializer)
{
serializer.Serialize(writer, value);
}
public override dynamic ReadJson(JsonReader reader, Type objectType, dynamic existingValue, JsonSerializer serializer)
{
if (reader.TokenType != JsonToken.StartObject) return reader.Value;
var node = JObject.Load(reader);
var definition = new ExpandoObject();
var definitionPropertyBag = definition as IDictionary<string, object>;
foreach (var property in node.Properties())
{
definitionPropertyBag[property.Name] = property.Value.ToObject<object>();
}
return JsonConvert.DeserializeAnonymousType(node.ToString(), definition);
}
public override bool CanConvert(Type _) => true;
}
@@ -0,0 +1,14 @@
using System;
namespace LCU.Library.Internal.DocumentModels.Converter;
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface)]
public class JsonKnownTypesAttribute : Attribute
{
public Type[] Subclasses { get; }
public JsonKnownTypesAttribute(params Type[] knownTypes)
{
Subclasses = knownTypes;
}
}
@@ -0,0 +1,48 @@
using System;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace LCU.Library.Internal.DocumentModels.Converter
{
public class FromJsonQueryAttribute : ModelBinderAttribute
{
public FromJsonQueryAttribute()
{
BinderType = typeof(JsonQueryBinder);
}
}
public class JsonQueryBinder : IModelBinder
{
public JsonQueryBinder()
{
}
public Task BindModelAsync(ModelBindingContext bindingContext)
{
var value = bindingContext.ValueProvider.GetValue(bindingContext.FieldName).FirstValue;
if (value == null)
{
return Task.CompletedTask;
}
try
{
var parsed = JsonSerializer.Deserialize(
value,
bindingContext.ModelType,
new JsonSerializerOptions(JsonSerializerDefaults.Web)
);
bindingContext.Result = ModelBindingResult.Success(parsed);
}
catch (Exception e)
{
bindingContext.Result = ModelBindingResult.Failed();
}
return Task.CompletedTask;
}
}
}
@@ -0,0 +1,107 @@
using System;
using System.Linq;
using System.Reflection;
using System.Text.Json;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using JsonSerializer = System.Text.Json.JsonSerializer;
namespace LCU.Library.Internal.DocumentModels.Converter;
internal static class PolymorphicJsonConverter
{
public const string TypeProperty = "_t";
public static Exception NotSupported<T>() => new NotSupportedException($"{typeof(T).Name} must specify known deserialization types");
public static bool IsSerializationSupportedFor<T>(Type actualType) => typeof(T).GetCustomAttribute<JsonKnownTypesAttribute>() is { } knownTypes
&& knownTypes.Subclasses.Contains(actualType);
public static Type DeserializationTypeFor<T>(string actualType) => typeof(T)
.GetCustomAttribute<JsonKnownTypesAttribute>()
.Subclasses
.Single(t => t.Name == actualType);
}
public class SystemPolymorphicJsonConverter<T> : System.Text.Json.Serialization.JsonConverter<T>
{
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var document = JsonDocument.ParseValue(ref reader);
var type = document.RootElement.GetProperty(PolymorphicJsonConverter.TypeProperty).GetString();
var deserializationType = PolymorphicJsonConverter.DeserializationTypeFor<T>(type);
return (T)document.Deserialize(deserializationType, options);
}
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
var actualType = value.GetType();
if (!PolymorphicJsonConverter.IsSerializationSupportedFor<T>(actualType))
{
throw PolymorphicJsonConverter.NotSupported<T>();
}
writer.WriteStartObject();
writer.WriteString(PolymorphicJsonConverter.TypeProperty, actualType.Name);
var actualValue = JsonSerializer.Serialize(value, actualType, options);
writer.WriteRawValue(actualValue.Substring(1, actualValue.Length - 2), true);
writer.WriteEndObject();
}
}
public class NewtonsoftPolymorphicJsonConverter<T> : Newtonsoft.Json.JsonConverter<T>
{
public override bool CanRead => _canRead;
private bool _canRead = true;
public override T ReadJson(JsonReader reader, Type objectType, T existingValue, bool hasExistingValue, Newtonsoft.Json.JsonSerializer serializer)
{
var document = JObject.Load(reader);
var type = document.GetValue(PolymorphicJsonConverter.TypeProperty)!.Value<string>();
var deserializationType = PolymorphicJsonConverter.DeserializationTypeFor<T>(type);
T result;
lock (this)
{
_canRead = false;
result = (T)JsonConvert.DeserializeObject(document.ToString(), deserializationType);
_canRead = true;
}
return result;
}
public override bool CanWrite => _canWrite;
private bool _canWrite = true;
public override void WriteJson(JsonWriter writer, T value, Newtonsoft.Json.JsonSerializer serializer)
{
var actualType = value.GetType();
if (!PolymorphicJsonConverter.IsSerializationSupportedFor<T>(actualType))
{
throw PolymorphicJsonConverter.NotSupported<T>();
}
writer.WriteStartObject();
writer.WritePropertyName(PolymorphicJsonConverter.TypeProperty);
writer.WriteValue(actualType.Name);
JObject jValue;
lock (this)
{
_canWrite = false;
jValue = JObject.FromObject(value);
_canWrite = true;
}
foreach (var property in jValue.Properties())
{
property.WriteTo(writer);
}
writer.WriteEndObject();
}
}
@@ -0,0 +1,86 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<Version>1.8.4</Version>
<PackageVersion>1.8.5</PackageVersion>
<Description>
1.0.0 - Initial Setup
1.0.1 - Delete Date is now an optional field in requests and responses, and source on update requests has been renamed to updateSource
1.0.2 - TraceId has been added as an optional field to documentInformation
1.0.3 - DocumentSearchRequest object has been added
1.0.4 - EncodeDocumentInformationFilterUri added to DocumentInformationFilter object
1.0.5 - Updating DocumentInformationResponse to have a JsonConverter for BigInt
1.0.6 - Separating JsonConverter for BigInt to own class
1.0.7 - Adding in DataSource field in request and response models and search filter
1.0.8 - Adding in CreatedDate field in request and response models and search filter
1.0.9 - Updating Filter Query Creation method to not shorten dates
1.1.0 - Add AccountNumbers, PersonNumbers and OrganizationNumbers to models and search filter
1.1.1 - Add document action notification route for actions on documents not fulfilled by Document API directly
1.1.2 - contract type definition for document audit logs
1.1.3 - document filter query string serialization fix
1.2.0 - Add Sorter to easily create sortable fields
1.2.1 - Change PersonNumberEquals and OrganizationNumberEquals to singular string
1.3.0 - Add DocumentInformationFilteredSearch
1.3.1 - fixed multiple serialization for DocumentFieldAudit Current and Previous Value properties
1.3.2 - fixed serialization of Current and Previous Value properties that aren't objects (e.g. null or boolean)
1.4.0 - document search filters can no longer be used in query strings, method EncodeDocumentInformationFilterUri removed
1.5.0 - Added document action for upload
1.5.1 - Removed ArchiveDate and DeleteDate from DocumentInformation
1.6.0 - Add GetDocumentCleanupRequest
1.7.0 - Add DocumentAction for Delete and DeleteContent, Add DeleteDocumentContentRequest
1.8.0 - Make DocumentInformation.DocumentBlobId a nullable Guid for deletion
1.8.1 - Add TypeContainsAny
1.8.2 - Add BusinessName to DocumentInformationFilter
1.8.3 - Reverted MongoDB.Driver version
1.8.4 - MongoDB.Driver version
1.8.5 - Nuget update DocumentIngestor Model
</Description>
<PackageReleaseNotes>
1.0.0 - Initial Setup
1.0.1 - Delete Date is now an optional field in requests and responses, and source on update requests has been renamed to updateSource
1.0.2 - TraceId has been added as an optional field to documentInformation
1.0.3 - DocumentSearchRequest object has been added
1.0.4 - EncodeDocumentInformationFilterUri added to DocumentInformationFilter object
1.0.5 - Updating DocumentInformationResponse to have a JsonConverter for BigInt
1.0.6 - Separating JsonConverter for BigInt to own class
1.0.7 - Adding in DataSource field in request and response models and search filter
1.0.8 - Adding in CreatedDate field in request and response models and search filter
1.0.9 - Updating Filter Query Creation method to not shorten dates
1.1.0 - Add AccountNumbers, PersonNumbers and OrganizationNumbers to models and search filter
1.1.1 - Add document action notification route for actions on documents not fulfilled by Document API directly
1.1.2 - contract type definition for document audit logs
1.1.3 - document filter query string serialization fix
1.2.0 - Add Sorter to easily create sortable fields
1.2.1 - Change PersonNumberEquals and OrganizationNumberEquals to singular string
1.3.0 - Add DocumentInformationFilteredSearch
1.3.1 - fixed multiple serialization for DocumentFieldAudit Current and Previous Value properties
1.3.2 - fixed serialization of Current and Previous Value properties that aren't objects (e.g. null or boolean)
1.4.0 - document search filters can no longer be used in query strings, method EncodeDocumentInformationFilterUri removed
1.5.0 - Added document action for upload
1.5.1 - Removed ArchiveDate and DeleteDate from DocumentInformation
1.6.0 - Add GetDocumentCleanupRequest
1.7.0 - Add DocumentAction for Delete and DeleteContent, Add DeleteDocumentContentRequest
1.8.0 - Make DocumentInformation.DocumentBlobId a nullable Guid for deletion
1.8.1 - Add TypeContainsAny
1.8.2 - Add BusinessName to DocumentInformationFilter
1.8.3 - Reverted MongoDB.Driver version
1.8.4 - MongoDB.Driver version
1.8.5 - Nuget update DocumentIngestor Model
</PackageReleaseNotes>
<RootNamespace>LCU.Library.Internal.DocumentModels</RootNamespace>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="LCU.Library.Internal.DocumentIngestorModels" Version="1.19.19" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.2.5" />
<PackageReference Include="Microsoft.OpenApi" Version="1.4.3" />
<PackageReference Include="MongoDB.Driver" Version="2.20.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
<PackageReference Include="System.Text.Json" Version="6.0.10" />
</ItemGroup>
</Project>
@@ -0,0 +1,11 @@
using System;
using System.ComponentModel.DataAnnotations;
namespace LCU.Library.Internal.DocumentModels.Requests;
public class DeleteDocumentContentRequest
{
[Required] public Guid DocumentId { get; set; }
public string? Reason { get; set; }
public string UserName { get; set; } = "System";
}
@@ -0,0 +1,9 @@
namespace LCU.Library.Internal.DocumentModels.Requests;
public enum DocumentAction
{
Download,
Upload,
Delete,
DeleteContent
}
@@ -0,0 +1,9 @@
using System.ComponentModel.DataAnnotations;
namespace LCU.Library.Internal.DocumentModels.Requests;
public class DocumentActionNotificationRequest
{
[Required] public DocumentAction Action { get; set; }
[Required] public string Source { get; set; }
}
@@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace LCU.Library.Internal.DocumentModels.Requests;
public class DocumentRequest
{
[Required] public string Name { get; set; }
[Required] public string Type { get; set; }
[Required] public byte[] Content { get; set; }
[Required] public string Source { get; set; }
[Required] public string DataSource { get; set; }
[Required] public DateTime CreatedDate { get; set; }
public Guid? TraceId { get; set; }
public string[] PersonNumbers { get; set; }
public string[] OrganizationNumbers { get; set; }
public string AccountNumber { get; set; }
public Dictionary<string, string> Attributes { get; set; }
}
@@ -0,0 +1,37 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Web;
using Microsoft.AspNetCore.Mvc;
namespace LCU.Library.Internal.DocumentModels.Requests.Search;
public class DocumentInformationFilter
{
public string? Name { get; set; }
public string? Type { get; set; }
public string? BusinessName { get; set; }
public string[]? TypeContainsAny { get; set; }
public bool? ScanStatus { get; set; }
public string? Source { get; set; }
public string? DataSource { get; set; }
public Guid? TraceId { get; set; }
public string? PersonNumbersEquals { get; set; }
public string[]? PersonNumbersContains { get; set; }
public string[]? PersonNumbersContainsAny { get; set; }
public string? OrganizationNumbersEquals { get; set; }
public string[]? OrganizationNumbersContains { get; set; }
public string[]? OrganizationNumbersContainsAny { get; set; }
public string? AccountNumberEquals { get; set; }
public string[]? AccountNumberContainsAny { get; set; }
public Dictionary<string, string>? AttributesEquals { get; set; }
public Dictionary<string, string>? AttributesContains { get; set; }
public Dictionary<string, string>? AttributesContainsAny { get; set; }
public DateTime? CreatedDateMinimum { get; set; }
public DateTime? CreatedDateMaximum { get; set; }
public DateTime? UploadDateMinimum { get; set; }
public DateTime? UploadDateMaximum { get; set; }
public DateTime? LastModifiedMinimum { get; set; }
public DateTime? LastModifiedDateMaximum { get; set; }
}
@@ -0,0 +1,12 @@
using System.Collections.Generic;
using LCU.Library.Internal.DocumentIngestorModels.Models.Pagination;
namespace LCU.Library.Internal.DocumentModels.Requests.Search;
public class DocumentInformationFilteredSearch
{
public DocumentInformationFilter Filter { get; set; }
public int? Skip { get; set; }
public int? Take { get; set; }
public List<SortableField>? SortDefinition { get; set; }
}
@@ -0,0 +1,10 @@
using System.Collections.Generic;
using LCU.Library.DocumentType.Models.Responses;
namespace LCU.Library.Internal.DocumentModels.Requests.Search;
public class GetDocumentCleanupRequest
{
public List<DocumentCodeDetailsResponse> DocumentCodeDetailInfos { get; set; }
public int Limit { get; set; }
}
@@ -0,0 +1,34 @@
using System.Collections.Generic;
using MongoDB.Driver;
using LCU.Library.Internal.DocumentIngestorModels.Models.Pagination;
namespace LCU.Library.Internal.DocumentModels.Requests.Search;
public class Sorter<T>
{
public static SortDefinition<T> GetSorter(List<SortableField> fields)
{
SortDefinition<T>? sortDefinition = null;
if(fields == null)
{
return sortDefinition;
}
fields.Sort((a, b) => a.Position.CompareTo(b.Position));
foreach (var field in fields)
{
if (sortDefinition == null)
{
sortDefinition = field.IsDescending ? Builders<T>.Sort.Descending(field.Field) : Builders<T>.Sort.Ascending(field.Field);
}
else
{
sortDefinition = field.IsDescending ? sortDefinition.Descending(field.Field) : sortDefinition.Ascending(field.Field);
}
}
return sortDefinition;
}
}
@@ -0,0 +1,10 @@
using System.ComponentModel.DataAnnotations;
namespace LCU.Library.Internal.DocumentModels.Requests;
public class UpdateDocumentContentRequest
{
[Required] public byte[] Content { get; set; }
[Required] public string UpdateSource { get; set; }
}
@@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace LCU.Library.Internal.DocumentModels.Requests;
public class UpdateDocumentInformationRequest
{
[Required] public string Name { get; set; }
[Required] public string Type { get; set; }
[Required] public string UpdateSource { get; set; }
public string[] PersonNumbers { get; set; }
public string[] OrganizationNumbers { get; set; }
public string AccountNumber { get; set; }
public Dictionary<string, string> Attributes { get; set; }
}
@@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace LCU.Library.Internal.DocumentModels.Requests;
public class UpdateDocumentRequest
{
[Required] public string Name { get; set; }
[Required] public string Type { get; set; }
[Required] public byte[] Content { get; set; }
[Required] public string UpdateSource { get; set; }
public string[] PersonNumbers { get; set; }
public string[] OrganizationNumbers { get; set; }
public string AccountNumber { get; set; }
public Dictionary<string, string> Attributes { get; set; }
}
@@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace LCU.Library.Internal.DocumentModels.Requests;
public class UpdateDocumentScanStatusRequest
{
[Required] public List<Guid> DocumentBlobIds { get; set; }
[Required] public bool ScanStatus { get; set; }
}
@@ -0,0 +1,33 @@
using LCU.Library.Internal.DocumentModels.Requests;
using System;
using LCU.Library.Internal.DocumentModels.Converter;
namespace LCU.Library.Internal.DocumentModels.Responses;
[JsonKnownTypes(typeof(DocumentFieldAuditResponse), typeof(DocumentActionAuditResponse))]
[System.Text.Json.Serialization.JsonConverter(typeof(SystemPolymorphicJsonConverter<DocumentAuditResponse>))]
[Newtonsoft.Json.JsonConverter(typeof(NewtonsoftPolymorphicJsonConverter<DocumentAuditResponse>))]
public abstract class DocumentAuditResponse
{
public Guid DocumentId { get; set; }
public string Source { get; set; }
public DateTime Date { get; set; }
public string UserId { get; set; }
}
public class DocumentFieldAuditResponse : DocumentAuditResponse
{
public string Field { get; set; }
[Newtonsoft.Json.JsonConverter(typeof(DynamicObjectJsonConverter))]
public dynamic PreviousValue { get; set; }
[Newtonsoft.Json.JsonConverter(typeof(DynamicObjectJsonConverter))]
public dynamic CurrentValue { get; set; }
}
public class DocumentActionAuditResponse : DocumentAuditResponse
{
public DocumentAction Action { get; set; }
public string ActionDescription { get; set; }
}

Some files were not shown because too many files have changed in this diff Show More