From 62a6cfe50b632ef5013ffb819e180dac10c715f1 Mon Sep 17 00:00:00 2001 From: Apollo3zehn Date: Thu, 7 Mar 2024 21:10:34 +0100 Subject: [PATCH 1/5] Formatting --- .editorconfig | 14 ++- .vscode/settings.json | 3 +- pyrightconfig.json | 2 +- src/Nexus.UI/ViewModels/SettingsViewModel.cs | 2 +- src/Nexus/API/CatalogsController.cs | 8 +- src/Nexus/API/UsersController.cs | 6 +- src/Nexus/Core/Models_NonPublic.cs | 4 +- ...ersonalAccessTokenAuthenticationHandler.cs | 6 +- .../DataSource/DataSourceController.cs | 4 +- .../PackageManagement/PackageController.cs | 22 ++-- src/Nexus/Program.cs | 2 +- src/Nexus/Services/DatabaseService.cs | 4 +- src/Nexus/Services/MemoryTracker.cs | 2 +- src/Nexus/Services/ProcessingService.cs | 12 +- src/Nexus/Services/TokenService.cs | 12 +- src/Nexus/Utilities/AuthUtilities.cs | 48 ++++---- src/Nexus/Utilities/BufferUtilities.cs | 6 +- src/Nexus/Utilities/GenericsUtilities.cs | 2 +- src/Nexus/Utilities/MemoryManager.cs | 2 +- src/Nexus/Utilities/NexusUtilities.cs | 2 +- .../DataModel/Representation.cs | 2 +- .../DataModel/Resource.cs | 6 +- .../DataModel/ResourceCatalog.cs | 4 +- .../Other/PackageControllerTests.cs | 105 ++++++++++++++++-- .../Services/MemoryTrackerTests.cs | 2 +- .../Nexus.Tests/Services/TokenServiceTests.cs | 6 +- tests/Nexus.Tests/myappsettings.json | 2 +- .../dotnet-client-tests/ClientTests.cs | 3 +- 28 files changed, 193 insertions(+), 100 deletions(-) diff --git a/.editorconfig b/.editorconfig index a4a437ba..8144ac9f 100755 --- a/.editorconfig +++ b/.editorconfig @@ -1,11 +1,19 @@ -# How to format: -# (1) Add dotnet_diagnostic.XXXX.severity = error -# (2) Run dotnet-format: dotnet format --diagnostics XXXX +# How to apply single rule: +# Run dotnet format --diagnostics XXXX --severity info + +# How to apply all rules: +# Run dotnet format --severity info/warning/error + +[*] +trim_trailing_whitespace = true [*.cs] # "run cleanup": https://betterprogramming.pub/enforce-net-code-style-with-editorconfig-d2f0d79091ac # TODO: build real editorconfig file: https://github.com/dotnet/roslyn/blob/main/.editorconfig +# Use file scoped namespace declarations +csharp_style_namespace_declarations = file_scoped:error + # Make field dotnet_diagnostic.IDE0044.severity = warning diff --git a/.vscode/settings.json b/.vscode/settings.json index 7940f672..cd510c2a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,5 +7,6 @@ "python.analysis.extraPaths": [ "src/clients/python-client" ], - "dotnet.defaultSolution": "Nexus.sln" + "dotnet.defaultSolution": "Nexus.sln", + "editor.formatOnSave": true } \ No newline at end of file diff --git a/pyrightconfig.json b/pyrightconfig.json index 906f6dea..9893e263 100644 --- a/pyrightconfig.json +++ b/pyrightconfig.json @@ -6,7 +6,7 @@ "tests/extensibility/python-extensibility-tests" ], "stubPath": "", - "executionEnvironments":[ + "executionEnvironments": [ { "root": ".", "extraPaths": [ diff --git a/src/Nexus.UI/ViewModels/SettingsViewModel.cs b/src/Nexus.UI/ViewModels/SettingsViewModel.cs index 7b5edf0b..34df75c3 100644 --- a/src/Nexus.UI/ViewModels/SettingsViewModel.cs +++ b/src/Nexus.UI/ViewModels/SettingsViewModel.cs @@ -283,7 +283,7 @@ private async Task InitializeAsync() // try restore saved file type var expectedFileType = _jsRuntime.Invoke("nexus.util.loadSetting", Constants.UI_FILE_TYPE_KEY); - if (!string.IsNullOrWhiteSpace(expectedFileType) && + if (!string.IsNullOrWhiteSpace(expectedFileType) && writerDescriptions.Any(writerDescription => writerDescription.Type == expectedFileType)) actualFileType = expectedFileType; diff --git a/src/Nexus/API/CatalogsController.cs b/src/Nexus/API/CatalogsController.cs index b4f91a09..271f3419 100644 --- a/src/Nexus/API/CatalogsController.cs +++ b/src/Nexus/API/CatalogsController.cs @@ -83,9 +83,9 @@ public async Task>> resourcePathAndRequests = await Task.WhenAll(resourcePaths.Distinct().Select(async resourcePath => { var catalogItemRequest = await root - .TryFindAsync(resourcePath, cancellationToken) + .TryFindAsync(resourcePath, cancellationToken) ?? throw new ValidationException($"Could not find resource path {resourcePath}."); - + return (resourcePath, catalogItemRequest); })); } @@ -532,7 +532,7 @@ private async Task> ProtectCatalogAsync( catalogContainer.Id, catalogContainer.Metadata, catalogContainer.Owner, User)) { return StatusCode( - StatusCodes.Status403Forbidden, + StatusCodes.Status403Forbidden, $"The current user is not permitted to read the catalog {catalogId}."); } @@ -540,7 +540,7 @@ private async Task> ProtectCatalogAsync( catalogContainer.Id, catalogContainer.Metadata, User)) { return StatusCode( - StatusCodes.Status403Forbidden, + StatusCodes.Status403Forbidden, $"The current user is not permitted to modify the catalog {catalogId}."); } diff --git a/src/Nexus/API/UsersController.cs b/src/Nexus/API/UsersController.cs index e54834d2..f3d34ed5 100644 --- a/src/Nexus/API/UsersController.cs +++ b/src/Nexus/API/UsersController.cs @@ -200,9 +200,9 @@ public async Task> CreateTokenAsync( var secret = await _tokenService .CreateAsync( - actualUserId, - token.Description, - utcExpires, + actualUserId, + token.Description, + utcExpires, token.Claims); var tokenValue = AuthUtilities.ComponentsToTokenValue(actualUserId, secret); diff --git a/src/Nexus/Core/Models_NonPublic.cs b/src/Nexus/Core/Models_NonPublic.cs index dff2c501..6670bdd9 100644 --- a/src/Nexus/Core/Models_NonPublic.cs +++ b/src/Nexus/Core/Models_NonPublic.cs @@ -5,12 +5,12 @@ namespace Nexus.Core { - internal record InternalPersonalAccessToken ( + internal record InternalPersonalAccessToken( Guid Id, string Description, DateTime Expires, IReadOnlyList Claims); - + internal record struct Interval( DateTime Begin, DateTime End); diff --git a/src/Nexus/Core/PersonalAccessTokenAuthenticationHandler.cs b/src/Nexus/Core/PersonalAccessTokenAuthenticationHandler.cs index 670eaf17..c68b4e60 100644 --- a/src/Nexus/Core/PersonalAccessTokenAuthenticationHandler.cs +++ b/src/Nexus/Core/PersonalAccessTokenAuthenticationHandler.cs @@ -22,8 +22,8 @@ internal class PersonalAccessTokenAuthHandler : AuthenticationHandler options, - ILoggerFactory logger, + IOptionsMonitor options, + ILoggerFactory logger, UrlEncoder encoder) : base(options, logger, encoder) { _tokenService = tokenService; @@ -75,7 +75,7 @@ protected async override Task HandleAuthenticateAsync() var identity = new ClaimsIdentity( claims, - Scheme.Name, + Scheme.Name, nameType: Claims.Name, roleType: Claims.Role); diff --git a/src/Nexus/Extensibility/DataSource/DataSourceController.cs b/src/Nexus/Extensibility/DataSource/DataSourceController.cs index 0baf1926..68f961e8 100644 --- a/src/Nexus/Extensibility/DataSource/DataSourceController.cs +++ b/src/Nexus/Extensibility/DataSource/DataSourceController.cs @@ -873,7 +873,7 @@ public static async Task ReadAsync( // of 10 minutes and a sample period of 10 ms, which leads to an estimated row size of 8 * 60000 = 480000 bytes. // The algorithm works this way because it cannot know if the data are already cached. It also does not know // if the data source will request more data which further increases the memory consumption. - + var bytesPerRow = 0L; var largestSamplePeriod = samplePeriod; @@ -1043,7 +1043,7 @@ await controller.ReadAsync( foreach (var catalogItemRequestPipeWriter in readingGroup.CatalogItemRequestPipeWriters) { cancellationToken.ThrowIfCancellationRequested(); - + await catalogItemRequestPipeWriter.DataWriter.CompleteAsync(); } } diff --git a/src/Nexus/PackageManagement/PackageController.cs b/src/Nexus/PackageManagement/PackageController.cs index 14fde8c5..c8000245 100644 --- a/src/Nexus/PackageManagement/PackageController.cs +++ b/src/Nexus/PackageManagement/PackageController.cs @@ -302,7 +302,7 @@ private async Task DiscoverGitTagsAsync(CancellationToken cancellation while (!process.StandardOutput.EndOfStream) { var refLine = await process.StandardOutput.ReadLineAsync(cancellationToken); - + try { var refString = refLine!.Split('\t')[1]; @@ -315,7 +315,7 @@ private async Task DiscoverGitTagsAsync(CancellationToken cancellation else { - _logger.LogDebug("Unable to extract tag from ref {Ref}", refLine); + _logger.LogDebug("Unable to extract tag from ref {Ref}", refLine); } } catch @@ -331,8 +331,8 @@ private async Task DiscoverGitTagsAsync(CancellationToken cancellation var escapedUriWithoutUserInfo = new Uri(repository) .GetComponents(UriComponents.AbsoluteUri & ~UriComponents.UserInfo, UriFormat.UriEscaped); - var error = process is null - ? default : + var error = process is null + ? default : $" Reason: {await process.StandardError.ReadToEndAsync(cancellationToken)}"; throw new Exception($"Unable to discover tags for repository {escapedUriWithoutUserInfo}.{error}"); @@ -391,8 +391,8 @@ private async Task RestoreGitTagAsync(string restoreRoot, CancellationTo var escapedUriWithoutUserInfo = new Uri(repository) .GetComponents(UriComponents.AbsoluteUri & ~UriComponents.UserInfo, UriFormat.UriEscaped); - var error = process1 is null - ? default : + var error = process1 is null + ? default : $" Reason: {await process1.StandardError.ReadToEndAsync(cancellationToken)}"; throw new Exception($"Unable to clone repository {escapedUriWithoutUserInfo}.{error}"); @@ -426,8 +426,8 @@ private async Task RestoreGitTagAsync(string restoreRoot, CancellationTo var escapedUriWithoutUserInfo = new Uri(repository) .GetComponents(UriComponents.AbsoluteUri & ~UriComponents.UserInfo, UriFormat.UriEscaped); - var error = process2 is null - ? default : + var error = process2 is null + ? default : $" Reason: {await process2.StandardError.ReadToEndAsync(cancellationToken)}"; throw new Exception($"Unable to publish project.{error}"); @@ -444,7 +444,7 @@ private async Task RestoreGitTagAsync(string restoreRoot, CancellationTo if (Directory.Exists(targetPath)) Directory.Delete(targetPath, recursive: true); } - catch {} + catch { } throw; } @@ -456,7 +456,7 @@ private async Task RestoreGitTagAsync(string restoreRoot, CancellationTo if (Directory.Exists(cloneFolder)) Directory.Delete(cloneFolder, recursive: true); } - catch {} + catch { } // try delete publish folder try @@ -464,7 +464,7 @@ private async Task RestoreGitTagAsync(string restoreRoot, CancellationTo if (Directory.Exists(publishFolder)) Directory.Delete(publishFolder, recursive: true); } - catch {} + catch { } } } else diff --git a/src/Nexus/Program.cs b/src/Nexus/Program.cs index 046cd085..cdfec304 100644 --- a/src/Nexus/Program.cs +++ b/src/Nexus/Program.cs @@ -223,7 +223,7 @@ void ConfigurePipeline(WebApplication app) // endpoints app.MapControllers(); - + // razor components app.MapRazorComponents() .AddInteractiveWebAssemblyRenderMode() diff --git a/src/Nexus/Services/DatabaseService.cs b/src/Nexus/Services/DatabaseService.cs index 5f686d77..666a875a 100644 --- a/src/Nexus/Services/DatabaseService.cs +++ b/src/Nexus/Services/DatabaseService.cs @@ -34,7 +34,7 @@ internal interface IDatabaseService /* /users */ bool TryReadTokenMap( - string userId, + string userId, [NotNullWhen(true)] out string? tokenMap); Stream WriteTokenMap( @@ -338,7 +338,7 @@ private static async Task DeleteCacheEntryAsync(string cacheEntry, TimeSpan time /* /users */ public bool TryReadTokenMap( - string userId, + string userId, [NotNullWhen(true)] out string? tokenMap) { var folderPath = SafePathCombine(_pathsOptions.Users, userId); diff --git a/src/Nexus/Services/MemoryTracker.cs b/src/Nexus/Services/MemoryTracker.cs index fc9fea6d..d2ad038b 100644 --- a/src/Nexus/Services/MemoryTracker.cs +++ b/src/Nexus/Services/MemoryTracker.cs @@ -118,7 +118,7 @@ public void UnregisterAllocation(AllocationRegistration allocationRegistration) private void SetConsumedBytesAndTriggerWaitingTasks(long difference) { _consumedBytes += difference; - + // allow all other waiting tasks to continue foreach (var retrySemaphore in _retrySemaphores) { diff --git a/src/Nexus/Services/ProcessingService.cs b/src/Nexus/Services/ProcessingService.cs index 912b422e..ea5fcd91 100644 --- a/src/Nexus/Services/ProcessingService.cs +++ b/src/Nexus/Services/ProcessingService.cs @@ -406,7 +406,7 @@ public static double Sum(Span data) return double.NaN; var sum = 0.0; - + for (int i = 0; i < data.Length; i++) { sum += data[i]; @@ -423,10 +423,10 @@ public static double Mean(Span data) var mean = 0.0; var m = 0UL; - + for (int i = 0; i < data.Length; i++) { - mean += (data[i] - mean)/++m; + mean += (data[i] - mean) / ++m; } return mean; @@ -488,11 +488,11 @@ public static double Variance(Span samples) for (int i = 1; i < samples.Length; i++) { t += samples[i]; - var diff = ((i + 1)*samples[i]) - t; + var diff = ((i + 1) * samples[i]) - t; variance += diff * diff / ((i + 1.0) * i); } - return variance/(samples.Length - 1); + return variance / (samples.Length - 1); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -506,7 +506,7 @@ public static double RootMeanSquare(Span data) for (int i = 0; i < data.Length; i++) { - mean += (data[i]*data[i] - mean)/++m; + mean += (data[i] * data[i] - mean) / ++m; } return Math.Sqrt(mean); diff --git a/src/Nexus/Services/TokenService.cs b/src/Nexus/Services/TokenService.cs index c564620a..343bc0ac 100644 --- a/src/Nexus/Services/TokenService.cs +++ b/src/Nexus/Services/TokenService.cs @@ -44,7 +44,7 @@ public TokenService(IDatabaseService databaseService) public Task CreateAsync( string userId, - string description, + string description, DateTime expires, IReadOnlyList claims) { @@ -109,8 +109,8 @@ public Task> GetAllAsyn string userId) { return InteractWithTokenMapAsync( - userId, - tokenMap => (IReadOnlyDictionary)tokenMap, + userId, + tokenMap => (IReadOnlyDictionary)tokenMap, saveChanges: false); } @@ -119,11 +119,11 @@ private ConcurrentDictionary GetTokenMap( { return _cache.GetOrAdd( userId, - key => + key => { if (_databaseService.TryReadTokenMap(userId, out var jsonString)) { - return JsonSerializer.Deserialize>(jsonString) + return JsonSerializer.Deserialize>(jsonString) ?? throw new Exception("tokenMap is null"); } @@ -135,7 +135,7 @@ private ConcurrentDictionary GetTokenMap( } private async Task InteractWithTokenMapAsync( - string userId, + string userId, Func, T> func, bool saveChanges) { diff --git a/src/Nexus/Utilities/AuthUtilities.cs b/src/Nexus/Utilities/AuthUtilities.cs index ed3acbe2..fdedb898 100644 --- a/src/Nexus/Utilities/AuthUtilities.cs +++ b/src/Nexus/Utilities/AuthUtilities.cs @@ -38,8 +38,8 @@ public static bool IsCatalogReadable( } public static bool IsCatalogWritable( - string catalogId, - CatalogMetadata catalogMetadata, + string catalogId, + CatalogMetadata catalogMetadata, ClaimsPrincipal user) { return InternalIsCatalogAccessible( @@ -54,9 +54,9 @@ public static bool IsCatalogWritable( } private static bool InternalIsCatalogAccessible( - string catalogId, - CatalogMetadata catalogMetadata, - ClaimsPrincipal? owner, + string catalogId, + CatalogMetadata catalogMetadata, + ClaimsPrincipal? owner, ClaimsPrincipal user, string singleClaimType, string groupClaimType, @@ -83,7 +83,7 @@ private static bool InternalIsCatalogAccessible( if (identity.AuthenticationType == PersonalAccessTokenAuthenticationDefaults.AuthenticationScheme) { var isAdmin = identity.HasClaim( - NexusClaims.ToPatUserClaimType(Claims.Role), + NexusClaims.ToPatUserClaimType(Claims.Role), NexusRoles.ADMINISTRATOR); if (isAdmin) @@ -91,7 +91,7 @@ private static bool InternalIsCatalogAccessible( /* The token alone can access the catalog ... */ var canAccessCatalog = identity.HasClaim( - claim => + claim => claim.Type == singleClaimType && Regex.IsMatch(catalogId, claim.Value) ); @@ -102,11 +102,11 @@ private static bool InternalIsCatalogAccessible( if (canAccessCatalog) { result = CanUserAccessCatalog( - catalogId, - catalogMetadata, - owner, + catalogId, + catalogMetadata, + owner, identity, - NexusClaims.ToPatUserClaimType(singleClaimType), + NexusClaims.ToPatUserClaimType(singleClaimType), NexusClaims.ToPatUserClaimType(groupClaimType)); } } @@ -115,7 +115,7 @@ private static bool InternalIsCatalogAccessible( else { var isAdmin = identity.HasClaim( - Claims.Role, + Claims.Role, NexusRoles.ADMINISTRATOR); if (isAdmin) @@ -123,11 +123,11 @@ private static bool InternalIsCatalogAccessible( /* ensure that user can read that catalog */ result = CanUserAccessCatalog( - catalogId, - catalogMetadata, - owner, - identity, - singleClaimType, + catalogId, + catalogMetadata, + owner, + identity, + singleClaimType, groupClaimType); } @@ -140,26 +140,26 @@ private static bool InternalIsCatalogAccessible( } private static bool CanUserAccessCatalog( - string catalogId, - CatalogMetadata catalogMetadata, - ClaimsPrincipal? owner, + string catalogId, + CatalogMetadata catalogMetadata, + ClaimsPrincipal? owner, ClaimsIdentity identity, string singleClaimType, string groupClaimType ) { - var isOwner = - owner is not null && + var isOwner = + owner is not null && owner?.FindFirstValue(Claims.Subject) == identity.FindFirst(Claims.Subject)?.Value; var canReadCatalog = identity.HasClaim( - claim => + claim => claim.Type == singleClaimType && Regex.IsMatch(catalogId, claim.Value) ); var canReadCatalogGroup = catalogMetadata.GroupMemberships is not null && identity.HasClaim( - claim => + claim => claim.Type == groupClaimType && catalogMetadata.GroupMemberships.Any(group => Regex.IsMatch(group, claim.Value)) ); diff --git a/src/Nexus/Utilities/BufferUtilities.cs b/src/Nexus/Utilities/BufferUtilities.cs index e8a194a3..8f1b99b9 100644 --- a/src/Nexus/Utilities/BufferUtilities.cs +++ b/src/Nexus/Utilities/BufferUtilities.cs @@ -11,7 +11,7 @@ public static void ApplyRepresentationStatusByDataType(NexusDataType dataType, R var targetType = NexusUtilities.GetTypeFromNexusDataType(dataType); var method = typeof(BufferUtilities) - .GetMethod(nameof(BufferUtilities.InternalApplyRepresentationStatusByDataType), BindingFlags.NonPublic | BindingFlags.Static)! + .GetMethod(nameof(InternalApplyRepresentationStatusByDataType), BindingFlags.NonPublic | BindingFlags.Static)! .MakeGenericMethod(targetType); method.Invoke(null, new object[] { data, status, target }); @@ -20,7 +20,7 @@ public static void ApplyRepresentationStatusByDataType(NexusDataType dataType, R private static void InternalApplyRepresentationStatusByDataType(ReadOnlyMemory data, ReadOnlyMemory status, Memory target) where T : unmanaged { - BufferUtilities.ApplyRepresentationStatus(data.Cast(), status, target); + ApplyRepresentationStatus(data.Cast(), status, target); } public static unsafe void ApplyRepresentationStatus(ReadOnlyMemory data, ReadOnlyMemory status, Memory target) where T : unmanaged @@ -31,7 +31,7 @@ public static unsafe void ApplyRepresentationStatus(ReadOnlyMemory data, R { fixed (double* targetPtr = target.Span) { - BufferUtilities.InternalApplyRepresentationStatus(target.Length, dataPtr, statusPtr, targetPtr); + InternalApplyRepresentationStatus(target.Length, dataPtr, statusPtr, targetPtr); } } } diff --git a/src/Nexus/Utilities/GenericsUtilities.cs b/src/Nexus/Utilities/GenericsUtilities.cs index 88d9193e..f25a3e9e 100644 --- a/src/Nexus/Utilities/GenericsUtilities.cs +++ b/src/Nexus/Utilities/GenericsUtilities.cs @@ -9,7 +9,7 @@ internal static class GenericToDouble private static Func EmitToDoubleConverter() { - var method = new DynamicMethod(string.Empty, typeof(double), new Type[] { typeof(T) }); + var method = new DynamicMethod(string.Empty, typeof(double), [typeof(T)]); var ilGenerator = method.GetILGenerator(); ilGenerator.Emit(OpCodes.Ldarg_0); diff --git a/src/Nexus/Utilities/MemoryManager.cs b/src/Nexus/Utilities/MemoryManager.cs index 6bcb4f70..931f1c29 100644 --- a/src/Nexus/Utilities/MemoryManager.cs +++ b/src/Nexus/Utilities/MemoryManager.cs @@ -24,4 +24,4 @@ protected override void Dispose(bool disposing) public override void Unpin() => throw new NotSupportedException(); } -} +} \ No newline at end of file diff --git a/src/Nexus/Utilities/NexusUtilities.cs b/src/Nexus/Utilities/NexusUtilities.cs index 76cb4e94..04e85466 100644 --- a/src/Nexus/Utilities/NexusUtilities.cs +++ b/src/Nexus/Utilities/NexusUtilities.cs @@ -12,7 +12,7 @@ internal static class NexusUtilities public static string DefaultBaseUrl { - get + get { if (_defaultBaseUrl is null) { diff --git a/src/extensibility/dotnet-extensibility/DataModel/Representation.cs b/src/extensibility/dotnet-extensibility/DataModel/Representation.cs index 12abd97a..beb5c586 100644 --- a/src/extensibility/dotnet-extensibility/DataModel/Representation.cs +++ b/src/extensibility/dotnet-extensibility/DataModel/Representation.cs @@ -118,7 +118,7 @@ public IReadOnlyDictionary? Parameters /// The representation kind. /// [JsonIgnore] - #warning Remove this when https://github.com/RicoSuter/NSwag/issues/4681 is solved +#warning Remove this when https://github.com/RicoSuter/NSwag/issues/4681 is solved internal RepresentationKind Kind { get; } /// diff --git a/src/extensibility/dotnet-extensibility/DataModel/Resource.cs b/src/extensibility/dotnet-extensibility/DataModel/Resource.cs index dce5bef3..35242a51 100644 --- a/src/extensibility/dotnet-extensibility/DataModel/Resource.cs +++ b/src/extensibility/dotnet-extensibility/DataModel/Resource.cs @@ -45,21 +45,21 @@ public Resource( /// Gets a regular expression to validate a resource identifier. /// [JsonIgnore] - #warning Remove this when https://github.com/RicoSuter/NSwag/issues/4681 is solved +#warning Remove this when https://github.com/RicoSuter/NSwag/issues/4681 is solved public static Regex ValidIdExpression { get; } = new Regex(@"^[a-zA-Z_][a-zA-Z_0-9]*$"); /// /// Gets a regular expression to find invalid characters in a resource identifier. /// [JsonIgnore] - #warning Remove this when https://github.com/RicoSuter/NSwag/issues/4681 is solved +#warning Remove this when https://github.com/RicoSuter/NSwag/issues/4681 is solved public static Regex InvalidIdCharsExpression { get; } = new Regex(@"[^a-zA-Z_0-9]", RegexOptions.Compiled); /// /// Gets a regular expression to find invalid start characters in a resource identifier. /// [JsonIgnore] - #warning Remove this when https://github.com/RicoSuter/NSwag/issues/4681 is solved +#warning Remove this when https://github.com/RicoSuter/NSwag/issues/4681 is solved public static Regex InvalidIdStartCharsExpression { get; } = new Regex(@"^[^a-zA-Z_]+", RegexOptions.Compiled); /// diff --git a/src/extensibility/dotnet-extensibility/DataModel/ResourceCatalog.cs b/src/extensibility/dotnet-extensibility/DataModel/ResourceCatalog.cs index 6c75786e..682b7f2a 100644 --- a/src/extensibility/dotnet-extensibility/DataModel/ResourceCatalog.cs +++ b/src/extensibility/dotnet-extensibility/DataModel/ResourceCatalog.cs @@ -47,11 +47,11 @@ public ResourceCatalog( /// Gets a regular expression to validate a resource catalog identifier. /// [JsonIgnore] - #warning Remove this when https://github.com/RicoSuter/NSwag/issues/4681 is solved +#warning Remove this when https://github.com/RicoSuter/NSwag/issues/4681 is solved public static Regex ValidIdExpression { get; } = new Regex(@"^(?:\/[a-zA-Z_][a-zA-Z_0-9]*)+$", RegexOptions.Compiled); [JsonIgnore] - #warning Remove this when https://github.com/RicoSuter/NSwag/issues/4681 is solved +#warning Remove this when https://github.com/RicoSuter/NSwag/issues/4681 is solved private static Regex _matchSingleParametersExpression { get; } = new Regex(@"\s*(.+?)\s*=\s*([^,\)]+)\s*,?", RegexOptions.Compiled); /// diff --git a/tests/Nexus.Tests/Other/PackageControllerTests.cs b/tests/Nexus.Tests/Other/PackageControllerTests.cs index 5a563438..d6e87c57 100644 --- a/tests/Nexus.Tests/Other/PackageControllerTests.cs +++ b/tests/Nexus.Tests/Other/PackageControllerTests.cs @@ -20,16 +20,99 @@ public class PackageControllerTests // is no real risk. private static readonly byte[] _token = [ - 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x5F, 0x70, 0x61, 0x74, - 0x5F, 0x31, 0x31, 0x41, 0x46, 0x41, 0x41, 0x45, 0x59, 0x49, - 0x30, 0x63, 0x55, 0x79, 0x35, 0x77, 0x72, 0x68, 0x38, 0x47, - 0x4E, 0x7A, 0x4B, 0x5F, 0x65, 0x4C, 0x33, 0x4F, 0x44, 0x39, - 0x30, 0x30, 0x4D, 0x52, 0x36, 0x4F, 0x62, 0x76, 0x50, 0x6E, - 0x6C, 0x58, 0x42, 0x36, 0x38, 0x50, 0x4B, 0x52, 0x37, 0x30, - 0x37, 0x68, 0x58, 0x30, 0x69, 0x56, 0x4B, 0x31, 0x57, 0x51, - 0x55, 0x39, 0x63, 0x67, 0x41, 0x4E, 0x73, 0x5A, 0x4E, 0x4F, - 0x55, 0x5A, 0x41, 0x50, 0x33, 0x4D, 0x51, 0x30, 0x67, 0x38, - 0x78, 0x58, 0x41 + 0x67, + 0x69, + 0x74, + 0x68, + 0x75, + 0x62, + 0x5F, + 0x70, + 0x61, + 0x74, + 0x5F, + 0x31, + 0x31, + 0x41, + 0x46, + 0x41, + 0x41, + 0x45, + 0x59, + 0x49, + 0x30, + 0x63, + 0x55, + 0x79, + 0x35, + 0x77, + 0x72, + 0x68, + 0x38, + 0x47, + 0x4E, + 0x7A, + 0x4B, + 0x5F, + 0x65, + 0x4C, + 0x33, + 0x4F, + 0x44, + 0x39, + 0x30, + 0x30, + 0x4D, + 0x52, + 0x36, + 0x4F, + 0x62, + 0x76, + 0x50, + 0x6E, + 0x6C, + 0x58, + 0x42, + 0x36, + 0x38, + 0x50, + 0x4B, + 0x52, + 0x37, + 0x30, + 0x37, + 0x68, + 0x58, + 0x30, + 0x69, + 0x56, + 0x4B, + 0x31, + 0x57, + 0x51, + 0x55, + 0x39, + 0x63, + 0x67, + 0x41, + 0x4E, + 0x73, + 0x5A, + 0x4E, + 0x4F, + 0x55, + 0x5A, + 0x41, + 0x50, + 0x33, + 0x4D, + 0x51, + 0x30, + 0x67, + 0x38, + 0x78, + 0x58, + 0x41 ]; #endregion @@ -252,7 +335,7 @@ public async Task CanRestore_local() #region Provider: git_tag -// Disable when running on GitHub Actions. It seems that there git ls-remote ignores the credentials (git clone works). + // Disable when running on GitHub Actions. It seems that there git ls-remote ignores the credentials (git clone works). #if !CI [Fact] public async Task CanDiscover_git_tag() diff --git a/tests/Nexus.Tests/Services/MemoryTrackerTests.cs b/tests/Nexus.Tests/Services/MemoryTrackerTests.cs index 025f5b4d..157a90ad 100644 --- a/tests/Nexus.Tests/Services/MemoryTrackerTests.cs +++ b/tests/Nexus.Tests/Services/MemoryTrackerTests.cs @@ -14,7 +14,7 @@ public async Task CanHandleMultipleRequests() // Arrange var weAreWaiting = new AutoResetEvent(initialState: false); var dataOptions = new DataOptions() { TotalBufferMemoryConsumption = 200 }; - + var memoryTracker = new MemoryTracker(Options.Create(dataOptions), NullLogger.Instance) { // TODO: remove this property and test with factor 8 diff --git a/tests/Nexus.Tests/Services/TokenServiceTests.cs b/tests/Nexus.Tests/Services/TokenServiceTests.cs index a41aad67..651deaf0 100644 --- a/tests/Nexus.Tests/Services/TokenServiceTests.cs +++ b/tests/Nexus.Tests/Services/TokenServiceTests.cs @@ -41,13 +41,13 @@ await tokenService.CreateAsync( var actualTokenMap = JsonSerializer.Deserialize>(jsonString)!; Assert.Collection( - actualTokenMap, - entry1 => + actualTokenMap, + entry1 => { Assert.Equal(description, entry1.Value.Description); Assert.Equal(expires, entry1.Value.Expires); - Assert.Collection(entry1.Value.Claims, + Assert.Collection(entry1.Value.Claims, entry1_1 => { Assert.Equal(claim1Type, entry1_1.Type); diff --git a/tests/Nexus.Tests/myappsettings.json b/tests/Nexus.Tests/myappsettings.json index fa87f981..93cc1f53 100644 --- a/tests/Nexus.Tests/myappsettings.json +++ b/tests/Nexus.Tests/myappsettings.json @@ -2,4 +2,4 @@ "Data": { "AggregationNaNThreshold": "0.90" } -} +} \ No newline at end of file diff --git a/tests/clients/dotnet-client-tests/ClientTests.cs b/tests/clients/dotnet-client-tests/ClientTests.cs index 49b68113..d3dc6cc6 100644 --- a/tests/clients/dotnet-client-tests/ClientTests.cs +++ b/tests/clients/dotnet-client-tests/ClientTests.cs @@ -72,7 +72,8 @@ public async Task CanAddConfigurationAsync() Assert.Collection(actualHeaders, Assert.Null, - headers => { + headers => + { Assert.NotNull(headers); Assert.Collection(headers, header => Assert.Equal(encodedJson, header)); }, From 0546e3795ea95b3e8374e93302363196ab0f1585 Mon Sep 17 00:00:00 2001 From: Apollo3zehn Date: Thu, 7 Mar 2024 21:13:48 +0100 Subject: [PATCH 2/5] Use file scoped namespaces --- .editorconfig | 2 +- .../Charts/AvailabilityChart.razor.cs | 211 +- src/Nexus.UI/Charts/Chart.razor.cs | 1541 ++++++++------- src/Nexus.UI/Charts/ChartTypes.cs | 123 +- src/Nexus.UI/Pages/ChartTest.razor.cs | 91 +- .../NexusAuthenticationStateProvider.cs | 69 +- src/Nexus.UI/Services/TypeFaceService.cs | 63 +- src/Nexus/API/ArtifactsController.cs | 81 +- src/Nexus/API/CatalogsController.cs | 941 +++++---- src/Nexus/API/DataController.cs | 113 +- src/Nexus/API/JobsController.cs | 523 +++-- src/Nexus/API/PackageReferencesController.cs | 199 +- src/Nexus/API/SourcesController.cs | 289 ++- src/Nexus/API/SystemController.cs | 135 +- src/Nexus/API/UsersController.cs | 677 ++++--- src/Nexus/API/WritersController.cs | 63 +- src/Nexus/Core/AppState.cs | 43 +- src/Nexus/Core/CacheEntryWrapper.cs | 381 ++-- src/Nexus/Core/CatalogCache.cs | 9 +- src/Nexus/Core/CatalogContainer.cs | 255 ++- src/Nexus/Core/CatalogContainerExtensions.cs | 109 +- src/Nexus/Core/CustomExtensions.cs | 103 +- .../Core/InternalControllerFeatureProvider.cs | 51 +- src/Nexus/Core/LoggerExtensions.cs | 31 +- src/Nexus/Core/Models_NonPublic.cs | 105 +- src/Nexus/Core/Models_Public.cs | 509 +++-- src/Nexus/Core/NexusAuthExtensions.cs | 337 ++-- src/Nexus/Core/NexusClaims.cs | 21 +- .../Core/NexusIdentityProviderExtensions.cs | 361 ++-- src/Nexus/Core/NexusOpenApiExtensions.cs | 121 +- src/Nexus/Core/NexusOptions.cs | 153 +- src/Nexus/Core/NexusPolicies.cs | 9 +- src/Nexus/Core/NexusRoles.cs | 11 +- src/Nexus/Core/StreamInputFormatter.cs | 19 +- src/Nexus/Core/UserDbContext.cs | 35 +- .../DataSource/DataSourceController.cs | 1755 ++++++++--------- .../DataSourceControllerExtensions.cs | 149 +- .../DataSource/DataSourceControllerTypes.cs | 17 +- .../DataSource/DataSourceDoubleStream.cs | 179 +- .../DataWriter/DataWriterController.cs | 429 ++-- .../DataWriter/DataWriterControllerTypes.cs | 11 +- src/Nexus/Extensions/Sources/Sample.cs | 347 ++-- src/Nexus/Extensions/Writers/Csv.cs | 527 +++-- src/Nexus/Extensions/Writers/CsvTypes.cs | 51 +- .../PackageManagement/PackageController.cs | 1325 +++++++------ .../PackageManagement/PackageLoadContext.cs | 57 +- src/Nexus/Services/AppStateManager.cs | 465 +++-- src/Nexus/Services/CacheService.cs | 315 ++- src/Nexus/Services/CatalogManager.cs | 507 +++-- src/Nexus/Services/DataControllerService.cs | 199 +- src/Nexus/Services/DataService.cs | 641 +++--- src/Nexus/Services/DatabaseService.cs | 559 +++--- src/Nexus/Services/DbService.cs | 187 +- src/Nexus/Services/ExtensionHive.cs | 329 ++- src/Nexus/Services/JobService.cs | 179 +- src/Nexus/Services/MemoryTracker.cs | 209 +- src/Nexus/Utilities/AuthUtilities.cs | 269 ++- src/Nexus/Utilities/BufferUtilities.cs | 61 +- src/Nexus/Utilities/GenericsUtilities.cs | 89 +- src/Nexus/Utilities/JsonSerializerHelper.cs | 39 +- src/Nexus/Utilities/MemoryManager.cs | 33 +- src/Nexus/Utilities/NexusUtilities.cs | 209 +- .../DataModel/DataModelExtensions.cs | 309 ++- .../DataModel/DataModelTypes.cs | 253 ++- .../DataModel/DataModelUtilities.cs | 401 ++-- .../DataModel/PropertiesExtensions.cs | 231 ++- .../DataModel/Representation.cs | 231 ++- .../DataModel/Resource.cs | 223 ++- .../DataModel/ResourceBuilder.cs | 179 +- .../DataModel/ResourceCatalog.cs | 321 ++- .../DataModel/ResourceCatalogBuilder.cs | 207 +- .../DataSource/DataSourceTypes.cs | 155 +- .../Extensibility/DataSource/IDataSource.cs | 145 +- .../DataSource/SimpleDataSource.cs | 119 +- .../DataWriter/DataWriterTypes.cs | 65 +- .../Extensibility/DataWriter/IDataWriter.cs | 101 +- .../Extensibility/ExtensibilityUtilities.cs | 57 +- .../ExtensionDescriptionAttribute.cs | 57 +- .../Extensibility/IExtension.cs | 15 +- .../DataSource/DataSourceControllerFixture.cs | 27 +- .../DataSource/DataSourceControllerTests.cs | 945 +++++---- .../DataSource/SampleDataSourceTests.cs | 227 ++- .../DataWriter/CsvDataWriterTests.cs | 309 ++- .../DataWriter/DataWriterControllerTests.cs | 271 ++- .../DataWriter/DataWriterFixture.cs | 95 +- .../Other/CacheEntryWrapperTests.cs | 291 ++- .../Other/CatalogContainersExtensionsTests.cs | 285 ++- tests/Nexus.Tests/Other/LoggingTests.cs | 267 ++- tests/Nexus.Tests/Other/OptionsTests.cs | 159 +- tests/Nexus.Tests/Other/UtilitiesTests.cs | 535 +++-- tests/TestExtensionProject/TestDataSource.cs | 53 +- tests/TestExtensionProject/TestDataWriter.cs | 37 +- .../dotnet-client-tests/ClientTests.cs | 121 +- .../DataModelExtensionsTests.cs | 149 +- .../DataModelFixture.cs | 279 ++- .../DataModelTests.cs | 541 +++-- 96 files changed, 12242 insertions(+), 12339 deletions(-) diff --git a/.editorconfig b/.editorconfig index 8144ac9f..05cea914 100755 --- a/.editorconfig +++ b/.editorconfig @@ -12,7 +12,7 @@ trim_trailing_whitespace = true # TODO: build real editorconfig file: https://github.com/dotnet/roslyn/blob/main/.editorconfig # Use file scoped namespace declarations -csharp_style_namespace_declarations = file_scoped:error +csharp_style_namespace_declarations = file_scoped:warning # Make field dotnet_diagnostic.IDE0044.severity = warning diff --git a/src/Nexus.UI/Charts/AvailabilityChart.razor.cs b/src/Nexus.UI/Charts/AvailabilityChart.razor.cs index b2f9a39c..2ccf4fb1 100644 --- a/src/Nexus.UI/Charts/AvailabilityChart.razor.cs +++ b/src/Nexus.UI/Charts/AvailabilityChart.razor.cs @@ -3,144 +3,143 @@ using SkiaSharp; using SkiaSharp.Views.Blazor; -namespace Nexus.UI.Charts +namespace Nexus.UI.Charts; + +public partial class AvailabilityChart { - public partial class AvailabilityChart - { - private const float LINE_HEIGHT = 7.0f; - private const float HALF_LINE_HEIGHT = LINE_HEIGHT / 2; + private const float LINE_HEIGHT = 7.0f; + private const float HALF_LINE_HEIGHT = LINE_HEIGHT / 2; - [Inject] - public TypeFaceService TypeFaceService { get; set; } = default!; + [Inject] + public TypeFaceService TypeFaceService { get; set; } = default!; - [Parameter] - public AvailabilityData AvailabilityData { get; set; } = default!; + [Parameter] + public AvailabilityData AvailabilityData { get; set; } = default!; - private void PaintSurface(SKPaintGLSurfaceEventArgs e) - { - /* sizes */ - var canvas = e.Surface.Canvas; - var surfaceSize = e.BackendRenderTarget.Size; + private void PaintSurface(SKPaintGLSurfaceEventArgs e) + { + /* sizes */ + var canvas = e.Surface.Canvas; + var surfaceSize = e.BackendRenderTarget.Size; - var yMin = LINE_HEIGHT * 2; - var yMax = (float)surfaceSize.Height; + var yMin = LINE_HEIGHT * 2; + var yMax = (float)surfaceSize.Height; - var xMin = 0.0f; - var xMax = (float)surfaceSize.Width; + var xMin = 0.0f; + var xMax = (float)surfaceSize.Width; - /* colors */ - using var barStrokePaint = new SKPaint - { - Style = SKPaintStyle.Stroke, - Color = new SKColor(249, 115, 22) - }; + /* colors */ + using var barStrokePaint = new SKPaint + { + Style = SKPaintStyle.Stroke, + Color = new SKColor(249, 115, 22) + }; - using var barFillPaint = new SKPaint - { - Color = new SKColor(249, 115, 22, 0x19) - }; + using var barFillPaint = new SKPaint + { + Color = new SKColor(249, 115, 22, 0x19) + }; - using var axisTitlePaint = new SKPaint - { - TextSize = 17, - IsAntialias = true, - Color = new SKColor(0x55, 0x55, 0x55), - TextAlign = SKTextAlign.Center - }; + using var axisTitlePaint = new SKPaint + { + TextSize = 17, + IsAntialias = true, + Color = new SKColor(0x55, 0x55, 0x55), + TextAlign = SKTextAlign.Center + }; - using var axisLabelPaint = new SKPaint - { - IsAntialias = true, - Typeface = TypeFaceService.GetTTF("Courier New Bold"), - Color = new SKColor(0x55, 0x55, 0x55) - }; + using var axisLabelPaint = new SKPaint + { + IsAntialias = true, + Typeface = TypeFaceService.GetTTF("Courier New Bold"), + Color = new SKColor(0x55, 0x55, 0x55) + }; - using var axisLabelCenteredPaint = new SKPaint - { - IsAntialias = true, - Typeface = TypeFaceService.GetTTF("Courier New Bold"), - Color = new SKColor(0x55, 0x55, 0x55), - TextAlign = SKTextAlign.Center - }; + using var axisLabelCenteredPaint = new SKPaint + { + IsAntialias = true, + Typeface = TypeFaceService.GetTTF("Courier New Bold"), + Color = new SKColor(0x55, 0x55, 0x55), + TextAlign = SKTextAlign.Center + }; - using var axisTickPaint = new SKPaint - { - Color = new SKColor(0xDD, 0xDD, 0xDD) - }; + using var axisTickPaint = new SKPaint + { + Color = new SKColor(0xDD, 0xDD, 0xDD) + }; - /* y-axis */ - var yRange = yMax - (yMin + 40); + /* y-axis */ + var yRange = yMax - (yMin + 40); - xMin += 20; + xMin += 20; - using (var canvasRestore = new SKAutoCanvasRestore(canvas)) - { - canvas.RotateDegrees(270, xMin, yMin + yRange / 2); - canvas.DrawText("Availability / %", new SKPoint(xMin, yMin + yRange / 2), axisTitlePaint); - } + using (var canvasRestore = new SKAutoCanvasRestore(canvas)) + { + canvas.RotateDegrees(270, xMin, yMin + yRange / 2); + canvas.DrawText("Availability / %", new SKPoint(xMin, yMin + yRange / 2), axisTitlePaint); + } - xMin += 10; + xMin += 10; - var widthPerCharacter = axisLabelPaint.MeasureText(" "); - var desiredYLabelCount = 11; - var maxYLabelCount = yRange / 50; - var ySkip = (int)(desiredYLabelCount / (float)maxYLabelCount) + 1; + var widthPerCharacter = axisLabelPaint.MeasureText(" "); + var desiredYLabelCount = 11; + var maxYLabelCount = yRange / 50; + var ySkip = (int)(desiredYLabelCount / (float)maxYLabelCount) + 1; - for (int i = 0; i < desiredYLabelCount; i++) + for (int i = 0; i < desiredYLabelCount; i++) + { + if ((i + ySkip) % ySkip == 0) { - if ((i + ySkip) % ySkip == 0) - { - var relative = i / 10.0f; - var y = yMin + (1 - relative) * yRange; - var label = $"{(int)(relative * 100),3:D0}"; - var lineOffset = widthPerCharacter * 3; - - canvas.DrawText(label, new SKPoint(xMin, y + HALF_LINE_HEIGHT), axisLabelPaint); - canvas.DrawLine(new SKPoint(xMin + lineOffset, y), new SKPoint(xMax, y), axisTickPaint); - } + var relative = i / 10.0f; + var y = yMin + (1 - relative) * yRange; + var label = $"{(int)(relative * 100),3:D0}"; + var lineOffset = widthPerCharacter * 3; + + canvas.DrawText(label, new SKPoint(xMin, y + HALF_LINE_HEIGHT), axisLabelPaint); + canvas.DrawLine(new SKPoint(xMin + lineOffset, y), new SKPoint(xMax, y), axisTickPaint); } + } - xMin += widthPerCharacter * 4; + xMin += widthPerCharacter * 4; - /* x-axis + data */ - var count = AvailabilityData.Data.Count; - var xRange = xMax - xMin; - var valueWidth = xRange / count; + /* x-axis + data */ + var count = AvailabilityData.Data.Count; + var xRange = xMax - xMin; + var valueWidth = xRange / count; - var maxXLabelCount = xRange / 200; - var xSkip = (int)(count / (float)maxXLabelCount) + 1; - var lastBegin = DateTime.MinValue; + var maxXLabelCount = xRange / 200; + var xSkip = (int)(count / (float)maxXLabelCount) + 1; + var lastBegin = DateTime.MinValue; - for (int i = 0; i < count; i++) - { - var availability = AvailabilityData.Data[i]; + for (int i = 0; i < count; i++) + { + var availability = AvailabilityData.Data[i]; - var x = xMin + i * valueWidth + valueWidth * 0.1f; - var y = yMin + yRange; - var w = valueWidth * 0.8f; - var h = -yRange * (float)availability; + var x = xMin + i * valueWidth + valueWidth * 0.1f; + var y = yMin + yRange; + var w = valueWidth * 0.8f; + var h = -yRange * (float)availability; - canvas.DrawRect(x, y, w, h, barFillPaint); + canvas.DrawRect(x, y, w, h, barFillPaint); - var path = new SKPath(); + var path = new SKPath(); - path.MoveTo(x, y); - path.RLineTo(0, h); - path.RLineTo(w, 0); - path.RLineTo(0, -h); + path.MoveTo(x, y); + path.RLineTo(0, h); + path.RLineTo(w, 0); + path.RLineTo(0, -h); - canvas.DrawPath(path, barStrokePaint); + canvas.DrawPath(path, barStrokePaint); - if ((i + xSkip) % xSkip == 0) - { - var currentBegin = AvailabilityData.Begin.AddDays(i); - canvas.DrawText(currentBegin.ToString("dd.MM"), xMin + (i + 0.5f) * valueWidth, yMax - 20, axisLabelCenteredPaint); + if ((i + xSkip) % xSkip == 0) + { + var currentBegin = AvailabilityData.Begin.AddDays(i); + canvas.DrawText(currentBegin.ToString("dd.MM"), xMin + (i + 0.5f) * valueWidth, yMax - 20, axisLabelCenteredPaint); - if (lastBegin.Year != currentBegin.Year) - canvas.DrawText(currentBegin.ToString("yyyy"), xMin + (i + 0.5f) * valueWidth, yMax, axisLabelCenteredPaint); + if (lastBegin.Year != currentBegin.Year) + canvas.DrawText(currentBegin.ToString("yyyy"), xMin + (i + 0.5f) * valueWidth, yMax, axisLabelCenteredPaint); - lastBegin = currentBegin; - } + lastBegin = currentBegin; } } } diff --git a/src/Nexus.UI/Charts/Chart.razor.cs b/src/Nexus.UI/Charts/Chart.razor.cs index 1b448213..b3c3a5d1 100644 --- a/src/Nexus.UI/Charts/Chart.razor.cs +++ b/src/Nexus.UI/Charts/Chart.razor.cs @@ -5,1015 +5,1014 @@ using SkiaSharp; using SkiaSharp.Views.Blazor; -namespace Nexus.UI.Charts +namespace Nexus.UI.Charts; + +public partial class Chart : IDisposable { - public partial class Chart : IDisposable - { - #region Fields + #region Fields - private SKGLView _skiaView = default!; - private readonly string _chartId = Guid.NewGuid().ToString(); - private Dictionary _axesMap = default!; + private SKGLView _skiaView = default!; + private readonly string _chartId = Guid.NewGuid().ToString(); + private Dictionary _axesMap = default!; - /* zoom */ - private bool _isDragging; - private readonly DotNetObjectReference _dotNetHelper; + /* zoom */ + private bool _isDragging; + private readonly DotNetObjectReference _dotNetHelper; - private SKRect _oldZoomBox; - private SKRect _zoomBox; - private Position _zoomStart; - private Position _zoomEnd; + private SKRect _oldZoomBox; + private SKRect _zoomBox; + private Position _zoomStart; + private Position _zoomEnd; - private DateTime _zoomedBegin; - private DateTime _zoomedEnd; + private DateTime _zoomedBegin; + private DateTime _zoomedEnd; - /* Common */ - private const float TICK_SIZE = 10; + /* Common */ + private const float TICK_SIZE = 10; - /* Y-Axis */ - private const float Y_PADDING_LEFT = 10; - private const float Y_PADDING_TOP = 20; - private const float Y_PADDING_Bottom = 25 + TIME_FAST_LABEL_OFFSET * 2; - private const float Y_UNIT_OFFSET = 30; - private const float TICK_MARGIN_LEFT = 5; + /* Y-Axis */ + private const float Y_PADDING_LEFT = 10; + private const float Y_PADDING_TOP = 20; + private const float Y_PADDING_Bottom = 25 + TIME_FAST_LABEL_OFFSET * 2; + private const float Y_UNIT_OFFSET = 30; + private const float TICK_MARGIN_LEFT = 5; - private const float AXIS_MARGIN_RIGHT = 5; - private const float HALF_LINE_HEIGHT = 3.5f; + private const float AXIS_MARGIN_RIGHT = 5; + private const float HALF_LINE_HEIGHT = 3.5f; - private readonly int[] _factors = new int[] { 2, 5, 10, 20, 50 }; + private readonly int[] _factors = new int[] { 2, 5, 10, 20, 50 }; - /* Time-Axis */ - private const float TIME_AXIS_MARGIN_TOP = 15; - private const float TIME_FAST_LABEL_OFFSET = 15; - private TimeAxisConfig _timeAxisConfig; - private readonly TimeAxisConfig[] _timeAxisConfigs; + /* Time-Axis */ + private const float TIME_AXIS_MARGIN_TOP = 15; + private const float TIME_FAST_LABEL_OFFSET = 15; + private TimeAxisConfig _timeAxisConfig; + private readonly TimeAxisConfig[] _timeAxisConfigs; - /* Others */ - private bool _beginAtZero; - private readonly SKColor[] _colors; + /* Others */ + private bool _beginAtZero; + private readonly SKColor[] _colors; - #endregion + #endregion - #region Constructors + #region Constructors - public Chart() - { - _dotNetHelper = DotNetObjectReference.Create(this); + public Chart() + { + _dotNetHelper = DotNetObjectReference.Create(this); - _timeAxisConfigs = new[] - { - /* nanoseconds */ - new TimeAxisConfig(TimeSpan.FromSeconds(100e-9), ".fffffff", TriggerPeriod.Second, "HH:mm.ss", "yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss.fffffff"), - - /* microseconds */ - new TimeAxisConfig(TimeSpan.FromSeconds(1e-6), ".ffffff", TriggerPeriod.Second, "HH:mm.ss", "yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss.fffffff"), - new TimeAxisConfig(TimeSpan.FromSeconds(5e-6), ".ffffff", TriggerPeriod.Second, "HH:mm.ss", "yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss.fffffff"), - new TimeAxisConfig(TimeSpan.FromSeconds(10e-6), ".ffffff", TriggerPeriod.Second, "HH:mm.ss", "yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss.fffffff"), - new TimeAxisConfig(TimeSpan.FromSeconds(50e-6), ".ffffff", TriggerPeriod.Second, "HH:mm.ss", "yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss.fffffff"), - new TimeAxisConfig(TimeSpan.FromSeconds(100e-6), ".ffffff", TriggerPeriod.Second, "HH:mm.ss", "yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss.fffffff"), - new TimeAxisConfig(TimeSpan.FromSeconds(500e-6), ".ffffff", TriggerPeriod.Second, "HH:mm.ss", "yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss.fffffff"), - - /* milliseconds */ - new TimeAxisConfig(TimeSpan.FromSeconds(1e-3), ".fff", TriggerPeriod.Minute, "HH:mm:ss", "yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss.ffffff"), - new TimeAxisConfig(TimeSpan.FromSeconds(5e-3), ".fff", TriggerPeriod.Minute, "HH:mm:ss", "yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss.ffffff"), - new TimeAxisConfig(TimeSpan.FromSeconds(10e-3), ".fff", TriggerPeriod.Minute, "HH:mm:ss", "yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss.fffff"), - new TimeAxisConfig(TimeSpan.FromSeconds(50e-3), ".fff", TriggerPeriod.Minute, "HH:mm:ss", "yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss.fffff"), - new TimeAxisConfig(TimeSpan.FromSeconds(100e-3), ".fff", TriggerPeriod.Minute, "HH:mm:ss", "yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss.ffff"), - new TimeAxisConfig(TimeSpan.FromSeconds(500e-3), ".fff", TriggerPeriod.Minute, "HH:mm:ss", "yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss.ffff"), - - /* seconds */ - new TimeAxisConfig(TimeSpan.FromSeconds(1), "HH:mm:ss", TriggerPeriod.Hour, "yyyy-MM-dd", default, "yyyy-MM-dd HH:mm:ss.fff"), - new TimeAxisConfig(TimeSpan.FromSeconds(5), "HH:mm:ss", TriggerPeriod.Hour, "yyyy-MM-dd", default, "yyyy-MM-dd HH:mm:ss.fff"), - new TimeAxisConfig(TimeSpan.FromSeconds(10), "HH:mm:ss", TriggerPeriod.Hour, "yyyy-MM-dd", default, "yyyy-MM-dd HH:mm:ss.fff"), - new TimeAxisConfig(TimeSpan.FromSeconds(30), "HH:mm:ss", TriggerPeriod.Hour, "yyyy-MM-dd", default, "yyyy-MM-dd HH:mm:ss.fff"), - - /* minutes */ - new TimeAxisConfig(TimeSpan.FromMinutes(1), "HH:mm", TriggerPeriod.Day, "yyyy-MM-dd", default, "yyyy-MM-dd HH:mm:ss"), - new TimeAxisConfig(TimeSpan.FromMinutes(5), "HH:mm", TriggerPeriod.Day, "yyyy-MM-dd", default, "yyyy-MM-dd HH:mm:ss"), - new TimeAxisConfig(TimeSpan.FromMinutes(10), "HH:mm", TriggerPeriod.Day, "yyyy-MM-dd", default, "yyyy-MM-dd HH:mm:ss"), - new TimeAxisConfig(TimeSpan.FromMinutes(30), "HH:mm", TriggerPeriod.Day, "yyyy-MM-dd", default, "yyyy-MM-dd HH:mm:ss"), - - /* hours */ - new TimeAxisConfig(TimeSpan.FromHours(1), "HH", TriggerPeriod.Day, "yyyy-MM-dd", default, "yyyy-MM-dd HH:mm"), - new TimeAxisConfig(TimeSpan.FromHours(3), "HH", TriggerPeriod.Day, "yyyy-MM-dd", default, "yyyy-MM-dd HH:mm"), - new TimeAxisConfig(TimeSpan.FromHours(6), "HH", TriggerPeriod.Day, "yyyy-MM-dd", default, "yyyy-MM-dd HH:mm"), - new TimeAxisConfig(TimeSpan.FromHours(12), "HH", TriggerPeriod.Day, "yyyy-MM-dd", default, "yyyy-MM-dd HH:mm"), - - /* days */ - new TimeAxisConfig(TimeSpan.FromDays(1), "dd", TriggerPeriod.Month, "yyyy-MM", default, "yyyy-MM-dd HH:mm"), - new TimeAxisConfig(TimeSpan.FromDays(10), "dd", TriggerPeriod.Month, "yyyy-MM", default, "yyyy-MM-dd HH"), - new TimeAxisConfig(TimeSpan.FromDays(30), "dd", TriggerPeriod.Month, "yyyy-MM", default, "yyyy-MM-dd HH"), - new TimeAxisConfig(TimeSpan.FromDays(90), "dd", TriggerPeriod.Month, "yyyy-MM", default, "yyyy-MM-dd HH"), - - /* years */ - new TimeAxisConfig(TimeSpan.FromDays(365), "yyyy", TriggerPeriod.Year, default, default, "yyyy-MM-dd"), - }; - - _timeAxisConfig = _timeAxisConfigs.First(); - - _colors = new[] { - new SKColor(0, 114, 189), - new SKColor(217, 83, 25), - new SKColor(237, 177, 32), - new SKColor(126, 47, 142), - new SKColor(119, 172, 48), - new SKColor(77, 190, 238), - new SKColor(162, 20, 47) - }; - } + _timeAxisConfigs = new[] + { + /* nanoseconds */ + new TimeAxisConfig(TimeSpan.FromSeconds(100e-9), ".fffffff", TriggerPeriod.Second, "HH:mm.ss", "yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss.fffffff"), + + /* microseconds */ + new TimeAxisConfig(TimeSpan.FromSeconds(1e-6), ".ffffff", TriggerPeriod.Second, "HH:mm.ss", "yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss.fffffff"), + new TimeAxisConfig(TimeSpan.FromSeconds(5e-6), ".ffffff", TriggerPeriod.Second, "HH:mm.ss", "yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss.fffffff"), + new TimeAxisConfig(TimeSpan.FromSeconds(10e-6), ".ffffff", TriggerPeriod.Second, "HH:mm.ss", "yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss.fffffff"), + new TimeAxisConfig(TimeSpan.FromSeconds(50e-6), ".ffffff", TriggerPeriod.Second, "HH:mm.ss", "yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss.fffffff"), + new TimeAxisConfig(TimeSpan.FromSeconds(100e-6), ".ffffff", TriggerPeriod.Second, "HH:mm.ss", "yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss.fffffff"), + new TimeAxisConfig(TimeSpan.FromSeconds(500e-6), ".ffffff", TriggerPeriod.Second, "HH:mm.ss", "yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss.fffffff"), + + /* milliseconds */ + new TimeAxisConfig(TimeSpan.FromSeconds(1e-3), ".fff", TriggerPeriod.Minute, "HH:mm:ss", "yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss.ffffff"), + new TimeAxisConfig(TimeSpan.FromSeconds(5e-3), ".fff", TriggerPeriod.Minute, "HH:mm:ss", "yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss.ffffff"), + new TimeAxisConfig(TimeSpan.FromSeconds(10e-3), ".fff", TriggerPeriod.Minute, "HH:mm:ss", "yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss.fffff"), + new TimeAxisConfig(TimeSpan.FromSeconds(50e-3), ".fff", TriggerPeriod.Minute, "HH:mm:ss", "yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss.fffff"), + new TimeAxisConfig(TimeSpan.FromSeconds(100e-3), ".fff", TriggerPeriod.Minute, "HH:mm:ss", "yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss.ffff"), + new TimeAxisConfig(TimeSpan.FromSeconds(500e-3), ".fff", TriggerPeriod.Minute, "HH:mm:ss", "yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss.ffff"), + + /* seconds */ + new TimeAxisConfig(TimeSpan.FromSeconds(1), "HH:mm:ss", TriggerPeriod.Hour, "yyyy-MM-dd", default, "yyyy-MM-dd HH:mm:ss.fff"), + new TimeAxisConfig(TimeSpan.FromSeconds(5), "HH:mm:ss", TriggerPeriod.Hour, "yyyy-MM-dd", default, "yyyy-MM-dd HH:mm:ss.fff"), + new TimeAxisConfig(TimeSpan.FromSeconds(10), "HH:mm:ss", TriggerPeriod.Hour, "yyyy-MM-dd", default, "yyyy-MM-dd HH:mm:ss.fff"), + new TimeAxisConfig(TimeSpan.FromSeconds(30), "HH:mm:ss", TriggerPeriod.Hour, "yyyy-MM-dd", default, "yyyy-MM-dd HH:mm:ss.fff"), + + /* minutes */ + new TimeAxisConfig(TimeSpan.FromMinutes(1), "HH:mm", TriggerPeriod.Day, "yyyy-MM-dd", default, "yyyy-MM-dd HH:mm:ss"), + new TimeAxisConfig(TimeSpan.FromMinutes(5), "HH:mm", TriggerPeriod.Day, "yyyy-MM-dd", default, "yyyy-MM-dd HH:mm:ss"), + new TimeAxisConfig(TimeSpan.FromMinutes(10), "HH:mm", TriggerPeriod.Day, "yyyy-MM-dd", default, "yyyy-MM-dd HH:mm:ss"), + new TimeAxisConfig(TimeSpan.FromMinutes(30), "HH:mm", TriggerPeriod.Day, "yyyy-MM-dd", default, "yyyy-MM-dd HH:mm:ss"), + + /* hours */ + new TimeAxisConfig(TimeSpan.FromHours(1), "HH", TriggerPeriod.Day, "yyyy-MM-dd", default, "yyyy-MM-dd HH:mm"), + new TimeAxisConfig(TimeSpan.FromHours(3), "HH", TriggerPeriod.Day, "yyyy-MM-dd", default, "yyyy-MM-dd HH:mm"), + new TimeAxisConfig(TimeSpan.FromHours(6), "HH", TriggerPeriod.Day, "yyyy-MM-dd", default, "yyyy-MM-dd HH:mm"), + new TimeAxisConfig(TimeSpan.FromHours(12), "HH", TriggerPeriod.Day, "yyyy-MM-dd", default, "yyyy-MM-dd HH:mm"), + + /* days */ + new TimeAxisConfig(TimeSpan.FromDays(1), "dd", TriggerPeriod.Month, "yyyy-MM", default, "yyyy-MM-dd HH:mm"), + new TimeAxisConfig(TimeSpan.FromDays(10), "dd", TriggerPeriod.Month, "yyyy-MM", default, "yyyy-MM-dd HH"), + new TimeAxisConfig(TimeSpan.FromDays(30), "dd", TriggerPeriod.Month, "yyyy-MM", default, "yyyy-MM-dd HH"), + new TimeAxisConfig(TimeSpan.FromDays(90), "dd", TriggerPeriod.Month, "yyyy-MM", default, "yyyy-MM-dd HH"), + + /* years */ + new TimeAxisConfig(TimeSpan.FromDays(365), "yyyy", TriggerPeriod.Year, default, default, "yyyy-MM-dd"), + }; + + _timeAxisConfig = _timeAxisConfigs.First(); + + _colors = new[] { + new SKColor(0, 114, 189), + new SKColor(217, 83, 25), + new SKColor(237, 177, 32), + new SKColor(126, 47, 142), + new SKColor(119, 172, 48), + new SKColor(77, 190, 238), + new SKColor(162, 20, 47) + }; + } - #endregion + #endregion - #region Properties + #region Properties - [Inject] - public TypeFaceService TypeFaceService { get; set; } = default!; + [Inject] + public TypeFaceService TypeFaceService { get; set; } = default!; - [Inject] - public IJSInProcessRuntime JSRuntime { get; set; } = default!; + [Inject] + public IJSInProcessRuntime JSRuntime { get; set; } = default!; - [Parameter] - public LineSeriesData LineSeriesData { get; set; } = default!; + [Parameter] + public LineSeriesData LineSeriesData { get; set; } = default!; - [Parameter] - public bool BeginAtZero + [Parameter] + public bool BeginAtZero + { + get { - get - { - return _beginAtZero; - } - set + return _beginAtZero; + } + set + { + if (value != _beginAtZero) { - if (value != _beginAtZero) - { - _beginAtZero = value; + _beginAtZero = value; - Task.Run(() => - { - _axesMap = LineSeriesData.Series - .GroupBy(lineSeries => lineSeries.Unit) - .ToDictionary(group => GetAxisInfo(group.Key, group), group => group.ToArray()); + Task.Run(() => + { + _axesMap = LineSeriesData.Series + .GroupBy(lineSeries => lineSeries.Unit) + .ToDictionary(group => GetAxisInfo(group.Key, group), group => group.ToArray()); - _skiaView.Invalidate(); - }); - } + _skiaView.Invalidate(); + }); } } + } - #endregion + #endregion - #region Callbacks + #region Callbacks - protected override void OnInitialized() + protected override void OnInitialized() + { + /* line series color */ + for (int i = 0; i < LineSeriesData.Series.Count; i++) { - /* line series color */ - for (int i = 0; i < LineSeriesData.Series.Count; i++) - { - var color = _colors[i % _colors.Length]; - LineSeriesData.Series[i].Color = color; - } - - /* axes info */ - _axesMap = LineSeriesData.Series - .GroupBy(lineSeries => lineSeries.Unit) - .ToDictionary(group => GetAxisInfo(group.Key, group), group => group.ToArray()); - - /* zoom */ - ResetZoom(); + var color = _colors[i % _colors.Length]; + LineSeriesData.Series[i].Color = color; } - private void OnMouseDown(MouseEventArgs e) - { - var position = JSRuntime.Invoke("nexus.chart.toRelative", _chartId, e.ClientX, e.ClientY); - _zoomStart = position; - _zoomEnd = position; + /* axes info */ + _axesMap = LineSeriesData.Series + .GroupBy(lineSeries => lineSeries.Unit) + .ToDictionary(group => GetAxisInfo(group.Key, group), group => group.ToArray()); - JSRuntime.InvokeVoid("nexus.util.addMouseUpEvent", _dotNetHelper); + /* zoom */ + ResetZoom(); + } - _isDragging = true; - } + private void OnMouseDown(MouseEventArgs e) + { + var position = JSRuntime.Invoke("nexus.chart.toRelative", _chartId, e.ClientX, e.ClientY); + _zoomStart = position; + _zoomEnd = position; - [JSInvokable] - public void OnMouseUp() - { - _isDragging = false; + JSRuntime.InvokeVoid("nexus.util.addMouseUpEvent", _dotNetHelper); - JSRuntime.InvokeVoid("nexus.chart.resize", _chartId, "selection", 0, 1, 0, 0); + _isDragging = true; + } - var zoomBox = CreateZoomBox(_zoomStart, _zoomEnd); + [JSInvokable] + public void OnMouseUp() + { + _isDragging = false; - if (zoomBox.Width > 0 && - zoomBox.Height > 0) - { - ApplyZoom(zoomBox); - _skiaView.Invalidate(); - } - } + JSRuntime.InvokeVoid("nexus.chart.resize", _chartId, "selection", 0, 1, 0, 0); - private void OnMouseMove(MouseEventArgs e) - { - var relativePosition = JSRuntime.Invoke("nexus.chart.toRelative", _chartId, e.ClientX, e.ClientY); - DrawAuxiliary(relativePosition); - } + var zoomBox = CreateZoomBox(_zoomStart, _zoomEnd); - private void OnMouseLeave(MouseEventArgs e) + if (zoomBox.Width > 0 && + zoomBox.Height > 0) { - JSRuntime.InvokeVoid("nexus.chart.hide", _chartId, "crosshairs-x"); - JSRuntime.InvokeVoid("nexus.chart.hide", _chartId, "crosshairs-y"); - - foreach (var series in LineSeriesData.Series) - { - JSRuntime.InvokeVoid("nexus.chart.hide", _chartId, $"pointer_{series.Id}"); - JSRuntime.InvokeVoid("nexus.chart.setTextContent", _chartId, $"value_{series.Id}", "--"); - } + ApplyZoom(zoomBox); + _skiaView.Invalidate(); } + } - private void OnDoubleClick(MouseEventArgs e) - { - ResetZoom(); - - var relativePosition = JSRuntime.Invoke("nexus.chart.toRelative", _chartId, e.ClientX, e.ClientY); - DrawAuxiliary(relativePosition); + private void OnMouseMove(MouseEventArgs e) + { + var relativePosition = JSRuntime.Invoke("nexus.chart.toRelative", _chartId, e.ClientX, e.ClientY); + DrawAuxiliary(relativePosition); + } - _skiaView.Invalidate(); - } + private void OnMouseLeave(MouseEventArgs e) + { + JSRuntime.InvokeVoid("nexus.chart.hide", _chartId, "crosshairs-x"); + JSRuntime.InvokeVoid("nexus.chart.hide", _chartId, "crosshairs-y"); - private void OnWheel(WheelEventArgs e) + foreach (var series in LineSeriesData.Series) { - const float FACTOR = 0.25f; + JSRuntime.InvokeVoid("nexus.chart.hide", _chartId, $"pointer_{series.Id}"); + JSRuntime.InvokeVoid("nexus.chart.setTextContent", _chartId, $"value_{series.Id}", "--"); + } + } - var relativePosition = JSRuntime.Invoke("nexus.chart.toRelative", _chartId, e.ClientX, e.ClientY); + private void OnDoubleClick(MouseEventArgs e) + { + ResetZoom(); - var zoomBox = new SKRect - { - Left = relativePosition.X * (e.DeltaY < 0 - ? +FACTOR // +0.25 - : -FACTOR), // -0.25 + var relativePosition = JSRuntime.Invoke("nexus.chart.toRelative", _chartId, e.ClientX, e.ClientY); + DrawAuxiliary(relativePosition); - Top = relativePosition.Y * (e.DeltaY < 0 - ? +FACTOR // +0.25 - : -FACTOR), // -0.25 + _skiaView.Invalidate(); + } - Right = relativePosition.X + (1 - relativePosition.X) * (e.DeltaY < 0 - ? (1 - FACTOR) // +0.75 - : (1 + FACTOR)), // +1.25 + private void OnWheel(WheelEventArgs e) + { + const float FACTOR = 0.25f; - Bottom = relativePosition.Y + (1 - relativePosition.Y) * (e.DeltaY < 0 - ? (1 - FACTOR) // +0.75 - : (1 + FACTOR)) // +1.25 - }; + var relativePosition = JSRuntime.Invoke("nexus.chart.toRelative", _chartId, e.ClientX, e.ClientY); + var zoomBox = new SKRect + { + Left = relativePosition.X * (e.DeltaY < 0 + ? +FACTOR // +0.25 + : -FACTOR), // -0.25 - ApplyZoom(zoomBox); - DrawAuxiliary(relativePosition); + Top = relativePosition.Y * (e.DeltaY < 0 + ? +FACTOR // +0.25 + : -FACTOR), // -0.25 - _skiaView.Invalidate(); - } + Right = relativePosition.X + (1 - relativePosition.X) * (e.DeltaY < 0 + ? (1 - FACTOR) // +0.75 + : (1 + FACTOR)), // +1.25 - private void ToggleSeriesEnabled(LineSeries series) - { - series.Show = !series.Show; - _skiaView.Invalidate(); - } + Bottom = relativePosition.Y + (1 - relativePosition.Y) * (e.DeltaY < 0 + ? (1 - FACTOR) // +0.75 + : (1 + FACTOR)) // +1.25 + }; - #endregion - #region Draw + ApplyZoom(zoomBox); + DrawAuxiliary(relativePosition); - private void PaintSurface(SKPaintGLSurfaceEventArgs e) - { - /* sizes */ - var canvas = e.Surface.Canvas; - var surfaceSize = e.BackendRenderTarget.Size; + _skiaView.Invalidate(); + } - var yMin = Y_PADDING_TOP; - var yMax = surfaceSize.Height - Y_PADDING_Bottom; - var xMin = Y_PADDING_LEFT; - var xMax = surfaceSize.Width; + private void ToggleSeriesEnabled(LineSeries series) + { + series.Show = !series.Show; + _skiaView.Invalidate(); + } - /* y-axis */ - xMin = DrawYAxes(canvas, xMin, yMin, yMax, _axesMap); - yMin += Y_UNIT_OFFSET; + #endregion - /* time-axis */ - DrawTimeAxis(canvas, xMin, yMin, xMax, yMax, _zoomedBegin, _zoomedEnd); + #region Draw - /* series */ - var dataBox = new SKRect(xMin, yMin, xMax, yMax); + private void PaintSurface(SKPaintGLSurfaceEventArgs e) + { + /* sizes */ + var canvas = e.Surface.Canvas; + var surfaceSize = e.BackendRenderTarget.Size; - using (var canvasRestore = new SKAutoCanvasRestore(canvas)) - { - canvas.ClipRect(dataBox); + var yMin = Y_PADDING_TOP; + var yMax = surfaceSize.Height - Y_PADDING_Bottom; + var xMin = Y_PADDING_LEFT; + var xMax = surfaceSize.Width; - /* for each axis */ - foreach (var axesEntry in _axesMap) - { - var axisInfo = axesEntry.Key; - var lineSeries = axesEntry.Value; + /* y-axis */ + xMin = DrawYAxes(canvas, xMin, yMin, yMax, _axesMap); + yMin += Y_UNIT_OFFSET; - /* for each dataset */ - foreach (var series in lineSeries) - { - var zoomInfo = GetZoomInfo(dataBox, _zoomBox, series.Data); - DrawSeries(canvas, zoomInfo, series, axisInfo); - } - } - } + /* time-axis */ + DrawTimeAxis(canvas, xMin, yMin, xMax, yMax, _zoomedBegin, _zoomedEnd); - /* overlay */ - JSRuntime.InvokeVoid( - "nexus.chart.resize", - _chartId, - "overlay", - dataBox.Left / surfaceSize.Width, - dataBox.Top / surfaceSize.Height, - dataBox.Right / surfaceSize.Width, - dataBox.Bottom / surfaceSize.Height); - } + /* series */ + var dataBox = new SKRect(xMin, yMin, xMax, yMax); - private void DrawAuxiliary(Position relativePosition) + using (var canvasRestore = new SKAutoCanvasRestore(canvas)) { - // datetime - var zoomedTimeRange = _zoomedEnd - _zoomedBegin; - var currentTimeBegin = _zoomedBegin + zoomedTimeRange * relativePosition.X; - var currentTimeBeginString = currentTimeBegin.ToString(_timeAxisConfig.CursorLabelFormat); - - JSRuntime.InvokeVoid("nexus.chart.setTextContent", _chartId, $"value_datetime", currentTimeBeginString); + canvas.ClipRect(dataBox); - // crosshairs - JSRuntime.InvokeVoid("nexus.chart.translate", _chartId, "crosshairs-x", 0, relativePosition.Y); - JSRuntime.InvokeVoid("nexus.chart.translate", _chartId, "crosshairs-y", relativePosition.X, 0); - - // points + /* for each axis */ foreach (var axesEntry in _axesMap) { var axisInfo = axesEntry.Key; var lineSeries = axesEntry.Value; - var dataRange = axisInfo.Max - axisInfo.Min; - var decimalDigits = Math.Max(0, -(int)Math.Round(Math.Log10(dataRange), MidpointRounding.AwayFromZero) + 2); - var formatString = $"F{decimalDigits}"; + /* for each dataset */ foreach (var series in lineSeries) { - var indexLeft = _zoomBox.Left * series.Data.Length; - var indexRight = _zoomBox.Right * series.Data.Length; - var indexRange = indexRight - indexLeft; - var index = indexLeft + relativePosition.X * indexRange; - var snappedIndex = (int)Math.Round(index, MidpointRounding.AwayFromZero); - - if (series.Show && snappedIndex < series.Data.Length) - { - var x = (snappedIndex - indexLeft) / indexRange; - var value = (float)series.Data[snappedIndex]; - var y = (value - axisInfo.Min) / (axisInfo.Max - axisInfo.Min); - - if (float.IsFinite(x) && 0 <= x && x <= 1 && - float.IsFinite(y) && 0 <= y && y <= 1) - { - JSRuntime.InvokeVoid("nexus.chart.translate", _chartId, $"pointer_{series.Id}", x, 1 - y); - - var valueString = string.IsNullOrWhiteSpace(series.Unit) - ? value.ToString(formatString) - : $"{value.ToString(formatString)} {@series.Unit}"; + var zoomInfo = GetZoomInfo(dataBox, _zoomBox, series.Data); + DrawSeries(canvas, zoomInfo, series, axisInfo); + } + } + } - JSRuntime.InvokeVoid("nexus.chart.setTextContent", _chartId, $"value_{series.Id}", valueString); + /* overlay */ + JSRuntime.InvokeVoid( + "nexus.chart.resize", + _chartId, + "overlay", + dataBox.Left / surfaceSize.Width, + dataBox.Top / surfaceSize.Height, + dataBox.Right / surfaceSize.Width, + dataBox.Bottom / surfaceSize.Height); + } - continue; - } - } + private void DrawAuxiliary(Position relativePosition) + { + // datetime + var zoomedTimeRange = _zoomedEnd - _zoomedBegin; + var currentTimeBegin = _zoomedBegin + zoomedTimeRange * relativePosition.X; + var currentTimeBeginString = currentTimeBegin.ToString(_timeAxisConfig.CursorLabelFormat); - JSRuntime.InvokeVoid("nexus.chart.hide", _chartId, $"pointer_{series.Id}"); - JSRuntime.InvokeVoid("nexus.chart.setTextContent", _chartId, $"value_{series.Id}", "--"); - } - } + JSRuntime.InvokeVoid("nexus.chart.setTextContent", _chartId, $"value_datetime", currentTimeBeginString); - // selection - if (_isDragging) - { - _zoomEnd = relativePosition; - var zoomBox = CreateZoomBox(_zoomStart, _zoomEnd); - - JSRuntime.InvokeVoid( - "nexus.chart.resize", - _chartId, - "selection", - zoomBox.Left, - zoomBox.Top, - zoomBox.Right, - zoomBox.Bottom); - } - } + // crosshairs + JSRuntime.InvokeVoid("nexus.chart.translate", _chartId, "crosshairs-x", 0, relativePosition.Y); + JSRuntime.InvokeVoid("nexus.chart.translate", _chartId, "crosshairs-y", relativePosition.X, 0); - private AxisInfo GetAxisInfo(string unit, IEnumerable lineDatasets) + // points + foreach (var axesEntry in _axesMap) { - var min = float.PositiveInfinity; - var max = float.NegativeInfinity; + var axisInfo = axesEntry.Key; + var lineSeries = axesEntry.Value; + var dataRange = axisInfo.Max - axisInfo.Min; + var decimalDigits = Math.Max(0, -(int)Math.Round(Math.Log10(dataRange), MidpointRounding.AwayFromZero) + 2); + var formatString = $"F{decimalDigits}"; - foreach (var lineDataset in lineDatasets) + foreach (var series in lineSeries) { - var data = lineDataset.Data; - var length = data.Length; + var indexLeft = _zoomBox.Left * series.Data.Length; + var indexRight = _zoomBox.Right * series.Data.Length; + var indexRange = indexRight - indexLeft; + var index = indexLeft + relativePosition.X * indexRange; + var snappedIndex = (int)Math.Round(index, MidpointRounding.AwayFromZero); - for (int i = 0; i < length; i++) + if (series.Show && snappedIndex < series.Data.Length) { - var value = (float)data[i]; + var x = (snappedIndex - indexLeft) / indexRange; + var value = (float)series.Data[snappedIndex]; + var y = (value - axisInfo.Min) / (axisInfo.Max - axisInfo.Min); - if (!double.IsNaN(value)) + if (float.IsFinite(x) && 0 <= x && x <= 1 && + float.IsFinite(y) && 0 <= y && y <= 1) { - if (value < min) - min = value; + JSRuntime.InvokeVoid("nexus.chart.translate", _chartId, $"pointer_{series.Id}", x, 1 - y); + + var valueString = string.IsNullOrWhiteSpace(series.Unit) + ? value.ToString(formatString) + : $"{value.ToString(formatString)} {@series.Unit}"; + + JSRuntime.InvokeVoid("nexus.chart.setTextContent", _chartId, $"value_{series.Id}", valueString); - if (value > max) - max = value; + continue; } } - } - if (min == double.PositiveInfinity || max == double.NegativeInfinity) - { - min = 0; - max = 0; + JSRuntime.InvokeVoid("nexus.chart.hide", _chartId, $"pointer_{series.Id}"); + JSRuntime.InvokeVoid("nexus.chart.setTextContent", _chartId, $"value_{series.Id}", "--"); } + } - GetYLimits(min, max, out var minLimit, out var maxLimit, out var _); + // selection + if (_isDragging) + { + _zoomEnd = relativePosition; + var zoomBox = CreateZoomBox(_zoomStart, _zoomEnd); - if (BeginAtZero) - { - if (minLimit > 0) - minLimit = 0; + JSRuntime.InvokeVoid( + "nexus.chart.resize", + _chartId, + "selection", + zoomBox.Left, + zoomBox.Top, + zoomBox.Right, + zoomBox.Bottom); + } + } - if (maxLimit < 0) - maxLimit = 0; - } + private AxisInfo GetAxisInfo(string unit, IEnumerable lineDatasets) + { + var min = float.PositiveInfinity; + var max = float.NegativeInfinity; + + foreach (var lineDataset in lineDatasets) + { + var data = lineDataset.Data; + var length = data.Length; - var axisInfo = new AxisInfo(unit, minLimit, maxLimit) + for (int i = 0; i < length; i++) { - Min = minLimit, - Max = maxLimit - }; + var value = (float)data[i]; + + if (!double.IsNaN(value)) + { + if (value < min) + min = value; - return axisInfo; + if (value > max) + max = value; + } + } } - #endregion + if (min == double.PositiveInfinity || max == double.NegativeInfinity) + { + min = 0; + max = 0; + } - #region Zoom + GetYLimits(min, max, out var minLimit, out var maxLimit, out var _); - private static ZoomInfo GetZoomInfo(SKRect dataBox, SKRect zoomBox, double[] data) + if (BeginAtZero) { - /* zoom x */ - var indexLeft = zoomBox.Left * data.Length; - var indexRight = zoomBox.Right * data.Length; - var indexRange = indexRight - indexLeft; + if (minLimit > 0) + minLimit = 0; - /* left */ - /* --> find left index of zoomed data and floor the result to include enough data in the final plot */ - var indexLeftRounded = (int)Math.Floor(indexLeft); - /* --> find how far left the most left data point is relative to the data box */ - var indexLeftShift = (indexLeft - indexLeftRounded) / indexRange; - var zoomedLeft = dataBox.Left - dataBox.Width * indexLeftShift; + if (maxLimit < 0) + maxLimit = 0; + } - /* right */ - /* --> find right index of zoomed data and ceil the result to include enough data in the final plot */ - var indexRightRounded = (int)Math.Ceiling(indexRight); - /* --> find how far right the most right data point is relative to the data box */ - var indexRightShift = (indexRightRounded - indexRight) / indexRange; - var zoomedRight = dataBox.Right + dataBox.Width * indexRightShift; + var axisInfo = new AxisInfo(unit, minLimit, maxLimit) + { + Min = minLimit, + Max = maxLimit + }; - /* create data array and data box */ - var intendedLength = (indexRightRounded + 1) - indexLeftRounded; - var zoomedData = data[indexLeftRounded..Math.Min((indexRightRounded + 1), data.Length)]; - var zoomedDataBox = new SKRect(zoomedLeft, dataBox.Top, zoomedRight, dataBox.Bottom); + return axisInfo; + } - /* A full series and a zoomed series are plotted differently: - * Full: Plot all data from dataBox.Left to dataBox.Right - 1 sample (no more data available, so it is impossible to draw more) - * Zoomed: Plot all data from dataBox.Left to dataBox.Right (this is possible because more data are available on the right) - */ - var isClippedRight = zoomedData.Length < intendedLength; + #endregion - return new ZoomInfo(zoomedData, zoomedDataBox, isClippedRight); - } + #region Zoom - private static SKRect CreateZoomBox(Position start, Position end) - { - var left = Math.Min(start.X, end.X); - var top = 0; - var right = Math.Max(start.X, end.X); - var bottom = 1; + private static ZoomInfo GetZoomInfo(SKRect dataBox, SKRect zoomBox, double[] data) + { + /* zoom x */ + var indexLeft = zoomBox.Left * data.Length; + var indexRight = zoomBox.Right * data.Length; + var indexRange = indexRight - indexLeft; + + /* left */ + /* --> find left index of zoomed data and floor the result to include enough data in the final plot */ + var indexLeftRounded = (int)Math.Floor(indexLeft); + /* --> find how far left the most left data point is relative to the data box */ + var indexLeftShift = (indexLeft - indexLeftRounded) / indexRange; + var zoomedLeft = dataBox.Left - dataBox.Width * indexLeftShift; + + /* right */ + /* --> find right index of zoomed data and ceil the result to include enough data in the final plot */ + var indexRightRounded = (int)Math.Ceiling(indexRight); + /* --> find how far right the most right data point is relative to the data box */ + var indexRightShift = (indexRightRounded - indexRight) / indexRange; + var zoomedRight = dataBox.Right + dataBox.Width * indexRightShift; + + /* create data array and data box */ + var intendedLength = (indexRightRounded + 1) - indexLeftRounded; + var zoomedData = data[indexLeftRounded..Math.Min((indexRightRounded + 1), data.Length)]; + var zoomedDataBox = new SKRect(zoomedLeft, dataBox.Top, zoomedRight, dataBox.Bottom); + + /* A full series and a zoomed series are plotted differently: + * Full: Plot all data from dataBox.Left to dataBox.Right - 1 sample (no more data available, so it is impossible to draw more) + * Zoomed: Plot all data from dataBox.Left to dataBox.Right (this is possible because more data are available on the right) + */ + var isClippedRight = zoomedData.Length < intendedLength; + + return new ZoomInfo(zoomedData, zoomedDataBox, isClippedRight); + } - return new SKRect(left, top, right, bottom); - } + private static SKRect CreateZoomBox(Position start, Position end) + { + var left = Math.Min(start.X, end.X); + var top = 0; + var right = Math.Max(start.X, end.X); + var bottom = 1; - private void ApplyZoom(SKRect zoomBox) - { - /* zoom box */ - var oldXRange = _oldZoomBox.Right - _oldZoomBox.Left; - var oldYRange = _oldZoomBox.Bottom - _oldZoomBox.Top; + return new SKRect(left, top, right, bottom); + } - var newZoomBox = new SKRect( - left: Math.Max(0, _oldZoomBox.Left + oldXRange * zoomBox.Left), - top: Math.Max(0, _oldZoomBox.Top + oldYRange * zoomBox.Top), - right: Math.Min(1, _oldZoomBox.Left + oldXRange * zoomBox.Right), - bottom: Math.Min(1, _oldZoomBox.Top + oldYRange * zoomBox.Bottom)); + private void ApplyZoom(SKRect zoomBox) + { + /* zoom box */ + var oldXRange = _oldZoomBox.Right - _oldZoomBox.Left; + var oldYRange = _oldZoomBox.Bottom - _oldZoomBox.Top; - if (newZoomBox.Width < 1e-6 || newZoomBox.Height < 1e-6) - return; + var newZoomBox = new SKRect( + left: Math.Max(0, _oldZoomBox.Left + oldXRange * zoomBox.Left), + top: Math.Max(0, _oldZoomBox.Top + oldYRange * zoomBox.Top), + right: Math.Min(1, _oldZoomBox.Left + oldXRange * zoomBox.Right), + bottom: Math.Min(1, _oldZoomBox.Top + oldYRange * zoomBox.Bottom)); - /* time range */ - var timeRange = LineSeriesData.End - LineSeriesData.Begin; + if (newZoomBox.Width < 1e-6 || newZoomBox.Height < 1e-6) + return; - _zoomedBegin = LineSeriesData.Begin + timeRange * newZoomBox.Left; - _zoomedEnd = LineSeriesData.Begin + timeRange * newZoomBox.Right; + /* time range */ + var timeRange = LineSeriesData.End - LineSeriesData.Begin; - /* data range */ - foreach (var axesEntry in _axesMap) - { - var axisInfo = axesEntry.Key; - var originalDataRange = axisInfo.OriginalMax - axisInfo.OriginalMin; + _zoomedBegin = LineSeriesData.Begin + timeRange * newZoomBox.Left; + _zoomedEnd = LineSeriesData.Begin + timeRange * newZoomBox.Right; - axisInfo.Min = axisInfo.OriginalMin + (1 - newZoomBox.Bottom) * originalDataRange; - axisInfo.Max = axisInfo.OriginalMax - newZoomBox.Top * originalDataRange; - } + /* data range */ + foreach (var axesEntry in _axesMap) + { + var axisInfo = axesEntry.Key; + var originalDataRange = axisInfo.OriginalMax - axisInfo.OriginalMin; - _oldZoomBox = newZoomBox; - _zoomBox = newZoomBox; + axisInfo.Min = axisInfo.OriginalMin + (1 - newZoomBox.Bottom) * originalDataRange; + axisInfo.Max = axisInfo.OriginalMax - newZoomBox.Top * originalDataRange; } - private void ResetZoom() - { - /* zoom box */ - _oldZoomBox = new SKRect(0, 0, 1, 1); - _zoomBox = new SKRect(0, 0, 1, 1); + _oldZoomBox = newZoomBox; + _zoomBox = newZoomBox; + } - /* time range */ - _zoomedBegin = LineSeriesData.Begin; - _zoomedEnd = LineSeriesData.End; + private void ResetZoom() + { + /* zoom box */ + _oldZoomBox = new SKRect(0, 0, 1, 1); + _zoomBox = new SKRect(0, 0, 1, 1); - /* data range */ - foreach (var axesEntry in _axesMap) - { - var axisInfo = axesEntry.Key; + /* time range */ + _zoomedBegin = LineSeriesData.Begin; + _zoomedEnd = LineSeriesData.End; - axisInfo.Min = axisInfo.OriginalMin; - axisInfo.Max = axisInfo.OriginalMax; - } + /* data range */ + foreach (var axesEntry in _axesMap) + { + var axisInfo = axesEntry.Key; + + axisInfo.Min = axisInfo.OriginalMin; + axisInfo.Max = axisInfo.OriginalMax; } + } - #endregion + #endregion - #region Y axis + #region Y axis - private float DrawYAxes(SKCanvas canvas, float xMin, float yMin, float yMax, Dictionary axesMap) + private float DrawYAxes(SKCanvas canvas, float xMin, float yMin, float yMax, Dictionary axesMap) + { + using var axisLabelPaint = new SKPaint { - using var axisLabelPaint = new SKPaint - { - Typeface = TypeFaceService.GetTTF("Courier New Bold"), - IsAntialias = true, - Color = new SKColor(0x55, 0x55, 0x55) - }; + Typeface = TypeFaceService.GetTTF("Courier New Bold"), + IsAntialias = true, + Color = new SKColor(0x55, 0x55, 0x55) + }; - using var axisTickPaint = new SKPaint - { - Color = new SKColor(0xDD, 0xDD, 0xDD), - IsAntialias = true - }; + using var axisTickPaint = new SKPaint + { + Color = new SKColor(0xDD, 0xDD, 0xDD), + IsAntialias = true + }; - var currentOffset = xMin; - var canvasRange = yMax - yMin; - var maxTickCount = Math.Max(1, (int)Math.Round(canvasRange / 50, MidpointRounding.AwayFromZero)); - var widthPerCharacter = axisLabelPaint.MeasureText(" "); + var currentOffset = xMin; + var canvasRange = yMax - yMin; + var maxTickCount = Math.Max(1, (int)Math.Round(canvasRange / 50, MidpointRounding.AwayFromZero)); + var widthPerCharacter = axisLabelPaint.MeasureText(" "); - foreach (var axesEntry in axesMap) - { - var axisInfo = axesEntry.Key; + foreach (var axesEntry in axesMap) + { + var axisInfo = axesEntry.Key; - /* get ticks */ - var ticks = GetYTicks(axisInfo.Min, axisInfo.Max, maxTickCount); - var dataRange = axisInfo.Max - axisInfo.Min; + /* get ticks */ + var ticks = GetYTicks(axisInfo.Min, axisInfo.Max, maxTickCount); + var dataRange = axisInfo.Max - axisInfo.Min; - /* get labels */ - var maxChars = axisInfo.Unit.Length; + /* get labels */ + var maxChars = axisInfo.Unit.Length; - var labels = ticks - .Select(tick => - { - var engineeringTick = ToEngineering(tick); - maxChars = Math.Max(maxChars, engineeringTick.Length); - return engineeringTick; - }) - .ToArray(); + var labels = ticks + .Select(tick => + { + var engineeringTick = ToEngineering(tick); + maxChars = Math.Max(maxChars, engineeringTick.Length); + return engineeringTick; + }) + .ToArray(); + + var textWidth = widthPerCharacter * maxChars; + var skipDraw = !axesEntry.Value.Any(lineSeries => lineSeries.Show); - var textWidth = widthPerCharacter * maxChars; - var skipDraw = !axesEntry.Value.Any(lineSeries => lineSeries.Show); + if (!skipDraw) + { + /* draw unit */ + var localUnitOffset = maxChars - axisInfo.Unit.Length; + var xUnit = currentOffset + localUnitOffset * widthPerCharacter; + var yUnit = yMin; + canvas.DrawText(axisInfo.Unit, new SKPoint(xUnit, yUnit), axisLabelPaint); - if (!skipDraw) + /* draw labels and ticks */ + for (int i = 0; i < ticks.Length; i++) { - /* draw unit */ - var localUnitOffset = maxChars - axisInfo.Unit.Length; - var xUnit = currentOffset + localUnitOffset * widthPerCharacter; - var yUnit = yMin; - canvas.DrawText(axisInfo.Unit, new SKPoint(xUnit, yUnit), axisLabelPaint); - - /* draw labels and ticks */ - for (int i = 0; i < ticks.Length; i++) - { - var tick = ticks[i]; + var tick = ticks[i]; - if (axisInfo.Min <= tick && tick <= axisInfo.Max) - { - var label = labels[i]; - var scaleFactor = (canvasRange - Y_UNIT_OFFSET) / dataRange; - var localLabelOffset = maxChars - label.Length; - var x = currentOffset + localLabelOffset * widthPerCharacter; - var y = yMax - (tick - axisInfo.Min) * scaleFactor; + if (axisInfo.Min <= tick && tick <= axisInfo.Max) + { + var label = labels[i]; + var scaleFactor = (canvasRange - Y_UNIT_OFFSET) / dataRange; + var localLabelOffset = maxChars - label.Length; + var x = currentOffset + localLabelOffset * widthPerCharacter; + var y = yMax - (tick - axisInfo.Min) * scaleFactor; - canvas.DrawText(label, new SKPoint(x, y + HALF_LINE_HEIGHT), axisLabelPaint); + canvas.DrawText(label, new SKPoint(x, y + HALF_LINE_HEIGHT), axisLabelPaint); - var tickX = currentOffset + textWidth + TICK_MARGIN_LEFT; - canvas.DrawLine(tickX, y, tickX + TICK_SIZE, y, axisTickPaint); - } + var tickX = currentOffset + textWidth + TICK_MARGIN_LEFT; + canvas.DrawLine(tickX, y, tickX + TICK_SIZE, y, axisTickPaint); } } - - /* update offset */ - currentOffset += textWidth + TICK_MARGIN_LEFT + TICK_SIZE + AXIS_MARGIN_RIGHT; } - return currentOffset - AXIS_MARGIN_RIGHT; + /* update offset */ + currentOffset += textWidth + TICK_MARGIN_LEFT + TICK_SIZE + AXIS_MARGIN_RIGHT; } - private static void GetYLimits(double min, double max, out float minLimit, out float maxLimit, out float step) - { - /* There are a minimum of 10 ticks and a maximum of 40 ticks with the following approach: - * - * Min Max Range Significant Min-Rounded Max-Rounded Start Step_1 ... End Count - * - * Min 0 32 32 2 0 100 0 10 ... 100 10 - * 968 1000 32 2 900 1000 900 910 ... 1000 10 - * - * Max 0 31 31 1 0 40 0 1 ... 40 40 - * 969 1000 31 1 960 1000 960 961 ... 1000 40 - */ - - /* special case: min == max */ - if (min == max) - { - min -= 0.5f; - max += 0.5f; - } + return currentOffset - AXIS_MARGIN_RIGHT; + } + + private static void GetYLimits(double min, double max, out float minLimit, out float maxLimit, out float step) + { + /* There are a minimum of 10 ticks and a maximum of 40 ticks with the following approach: + * + * Min Max Range Significant Min-Rounded Max-Rounded Start Step_1 ... End Count + * + * Min 0 32 32 2 0 100 0 10 ... 100 10 + * 968 1000 32 2 900 1000 900 910 ... 1000 10 + * + * Max 0 31 31 1 0 40 0 1 ... 40 40 + * 969 1000 31 1 960 1000 960 961 ... 1000 40 + */ + + /* special case: min == max */ + if (min == max) + { + min -= 0.5f; + max += 0.5f; + } + + /* range and position of first significant digit */ + var range = max - min; + var significant = (int)Math.Round(Math.Log10(range), MidpointRounding.AwayFromZero); - /* range and position of first significant digit */ - var range = max - min; - var significant = (int)Math.Round(Math.Log10(range), MidpointRounding.AwayFromZero); + /* get limits */ + minLimit = (float)RoundDown(min, decimalPlaces: -significant); + maxLimit = (float)RoundUp(max, decimalPlaces: -significant); - /* get limits */ + /* special case: min == minLimit */ + if (min == minLimit) + { + min -= range / 8; minLimit = (float)RoundDown(min, decimalPlaces: -significant); + } + + /* special case: max == maxLimit */ + if (max == maxLimit) + { + max += range / 8; maxLimit = (float)RoundUp(max, decimalPlaces: -significant); + } - /* special case: min == minLimit */ - if (min == minLimit) - { - min -= range / 8; - minLimit = (float)RoundDown(min, decimalPlaces: -significant); - } + /* get tick step */ + step = (float)Math.Pow(10, significant - 1); + } - /* special case: max == maxLimit */ - if (max == maxLimit) - { - max += range / 8; - maxLimit = (float)RoundUp(max, decimalPlaces: -significant); - } + private float[] GetYTicks(float min, float max, int maxTickCount) + { + GetYLimits(min, max, out var minLimit, out var maxLimit, out var step); - /* get tick step */ - step = (float)Math.Pow(10, significant - 1); - } + var range = maxLimit - minLimit; + var tickCount = (int)Math.Ceiling((range / step) + 1); - private float[] GetYTicks(float min, float max, int maxTickCount) + /* ensure there are not too many ticks */ + if (tickCount > maxTickCount) { - GetYLimits(min, max, out var minLimit, out var maxLimit, out var step); + var originalStep = step; + var originalTickCount = tickCount; - var range = maxLimit - minLimit; - var tickCount = (int)Math.Ceiling((range / step) + 1); - - /* ensure there are not too many ticks */ - if (tickCount > maxTickCount) + for (int i = 0; i < _factors.Length; i++) { - var originalStep = step; - var originalTickCount = tickCount; - - for (int i = 0; i < _factors.Length; i++) - { - var factor = _factors[i]; + var factor = _factors[i]; - tickCount = (int)Math.Ceiling(originalTickCount / (float)factor); - step = originalStep * factor; + tickCount = (int)Math.Ceiling(originalTickCount / (float)factor); + step = originalStep * factor; - if (tickCount <= maxTickCount) - break; - } + if (tickCount <= maxTickCount) + break; } + } - if (tickCount > maxTickCount) - throw new Exception("Unable to calculate Y-axis ticks."); + if (tickCount > maxTickCount) + throw new Exception("Unable to calculate Y-axis ticks."); - /* calculate actual steps */ - return Enumerable - .Range(0, tickCount) - .Select(tickNumber => (float)(minLimit + tickNumber * step)) - .ToArray(); - } + /* calculate actual steps */ + return Enumerable + .Range(0, tickCount) + .Select(tickNumber => (float)(minLimit + tickNumber * step)) + .ToArray(); + } - #endregion + #endregion - #region Time axis + #region Time axis - private void DrawTimeAxis(SKCanvas canvas, float xMin, float yMin, float xMax, float yMax, DateTime begin, DateTime end) + private void DrawTimeAxis(SKCanvas canvas, float xMin, float yMin, float xMax, float yMax, DateTime begin, DateTime end) + { + using var axisLabelPaint = new SKPaint { - using var axisLabelPaint = new SKPaint - { - Typeface = TypeFaceService.GetTTF("Courier New Bold"), - TextAlign = SKTextAlign.Center, - IsAntialias = true, - Color = new SKColor(0x55, 0x55, 0x55) - }; + Typeface = TypeFaceService.GetTTF("Courier New Bold"), + TextAlign = SKTextAlign.Center, + IsAntialias = true, + Color = new SKColor(0x55, 0x55, 0x55) + }; - using var axisTickPaint = new SKPaint - { - Color = SKColors.LightGray, - IsAntialias = true - }; + using var axisTickPaint = new SKPaint + { + Color = SKColors.LightGray, + IsAntialias = true + }; - var canvasRange = xMax - xMin; - var maxTickCount = Math.Max(1, (int)Math.Round(canvasRange / 130, MidpointRounding.AwayFromZero)); - var (config, ticks) = GetTimeTicks(begin, end, maxTickCount); - _timeAxisConfig = config; + var canvasRange = xMax - xMin; + var maxTickCount = Math.Max(1, (int)Math.Round(canvasRange / 130, MidpointRounding.AwayFromZero)); + var (config, ticks) = GetTimeTicks(begin, end, maxTickCount); + _timeAxisConfig = config; - var timeRange = (end - begin).Ticks; - var scalingFactor = canvasRange / timeRange; - var previousTick = DateTime.MinValue; + var timeRange = (end - begin).Ticks; + var scalingFactor = canvasRange / timeRange; + var previousTick = DateTime.MinValue; - foreach (var tick in ticks) - { - /* vertical line */ - var x = xMin + (tick - begin).Ticks * scalingFactor; - canvas.DrawLine(x, yMin, x, yMax + TICK_SIZE, axisTickPaint); + foreach (var tick in ticks) + { + /* vertical line */ + var x = xMin + (tick - begin).Ticks * scalingFactor; + canvas.DrawLine(x, yMin, x, yMax + TICK_SIZE, axisTickPaint); - /* fast tick */ - var tickLabel = tick.ToString(config.FastTickLabelFormat); - canvas.DrawText(tickLabel, x, yMax + TICK_SIZE + TIME_AXIS_MARGIN_TOP, axisLabelPaint); + /* fast tick */ + var tickLabel = tick.ToString(config.FastTickLabelFormat); + canvas.DrawText(tickLabel, x, yMax + TICK_SIZE + TIME_AXIS_MARGIN_TOP, axisLabelPaint); - /* slow tick */ - var addSlowTick = IsSlowTickRequired(previousTick, tick, config.SlowTickTrigger); + /* slow tick */ + var addSlowTick = IsSlowTickRequired(previousTick, tick, config.SlowTickTrigger); - if (addSlowTick) + if (addSlowTick) + { + if (config.SlowTickLabelFormat1 is not null) { - if (config.SlowTickLabelFormat1 is not null) - { - var slowTickLabel1 = tick.ToString(config.SlowTickLabelFormat1); - canvas.DrawText(slowTickLabel1, x, yMax + TICK_SIZE + TIME_AXIS_MARGIN_TOP + TIME_FAST_LABEL_OFFSET, axisLabelPaint); - } - - if (config.SlowTickLabelFormat2 is not null) - { - var slowTickLabel2 = tick.ToString(config.SlowTickLabelFormat2); - canvas.DrawText(slowTickLabel2, x, yMax + TICK_SIZE + TIME_AXIS_MARGIN_TOP + TIME_FAST_LABEL_OFFSET * 2, axisLabelPaint); - } + var slowTickLabel1 = tick.ToString(config.SlowTickLabelFormat1); + canvas.DrawText(slowTickLabel1, x, yMax + TICK_SIZE + TIME_AXIS_MARGIN_TOP + TIME_FAST_LABEL_OFFSET, axisLabelPaint); } - /* */ - previousTick = tick; + if (config.SlowTickLabelFormat2 is not null) + { + var slowTickLabel2 = tick.ToString(config.SlowTickLabelFormat2); + canvas.DrawText(slowTickLabel2, x, yMax + TICK_SIZE + TIME_AXIS_MARGIN_TOP + TIME_FAST_LABEL_OFFSET * 2, axisLabelPaint); + } } + + /* */ + previousTick = tick; } + } - private (TimeAxisConfig, DateTime[]) GetTimeTicks(DateTime begin, DateTime end, int maxTickCount) - { - static long GetTickCount(DateTime begin, DateTime end, TimeSpan tickInterval) - => (long)Math.Ceiling((end - begin) / tickInterval); + private (TimeAxisConfig, DateTime[]) GetTimeTicks(DateTime begin, DateTime end, int maxTickCount) + { + static long GetTickCount(DateTime begin, DateTime end, TimeSpan tickInterval) + => (long)Math.Ceiling((end - begin) / tickInterval); - /* find TimeAxisConfig */ - TimeAxisConfig? selectedConfig = default; + /* find TimeAxisConfig */ + TimeAxisConfig? selectedConfig = default; - foreach (var config in _timeAxisConfigs) - { - var currentTickCount = GetTickCount(begin, end, config.TickInterval); + foreach (var config in _timeAxisConfigs) + { + var currentTickCount = GetTickCount(begin, end, config.TickInterval); - if (currentTickCount <= maxTickCount) - { - selectedConfig = config; - break; - } + if (currentTickCount <= maxTickCount) + { + selectedConfig = config; + break; } + } - /* ensure TIME_MAX_TICK_COUNT is not exceeded */ - selectedConfig ??= _timeAxisConfigs.Last(); + /* ensure TIME_MAX_TICK_COUNT is not exceeded */ + selectedConfig ??= _timeAxisConfigs.Last(); - var tickInterval = selectedConfig.TickInterval; - var tickCount = GetTickCount(begin, end, tickInterval); + var tickInterval = selectedConfig.TickInterval; + var tickCount = GetTickCount(begin, end, tickInterval); - while (tickCount > maxTickCount) - { - tickInterval *= 2; - tickCount = GetTickCount(begin, end, tickInterval); - } + while (tickCount > maxTickCount) + { + tickInterval *= 2; + tickCount = GetTickCount(begin, end, tickInterval); + } - /* calculate ticks */ - var firstTick = RoundUp(begin, tickInterval); + /* calculate ticks */ + var firstTick = RoundUp(begin, tickInterval); - var ticks = Enumerable - .Range(0, (int)tickCount) - .Select(tickIndex => firstTick + tickIndex * tickInterval) - .Where(tick => tick < end) - .ToArray(); + var ticks = Enumerable + .Range(0, (int)tickCount) + .Select(tickIndex => firstTick + tickIndex * tickInterval) + .Where(tick => tick < end) + .ToArray(); - return (selectedConfig, ticks); - } + return (selectedConfig, ticks); + } - private static bool IsSlowTickRequired(DateTime previousTick, DateTime tick, TriggerPeriod trigger) + private static bool IsSlowTickRequired(DateTime previousTick, DateTime tick, TriggerPeriod trigger) + { + return trigger switch { - return trigger switch - { - TriggerPeriod.Second => previousTick.Date != tick.Date || - previousTick.Hour != tick.Hour || - previousTick.Minute != tick.Minute || - previousTick.Second != tick.Second, + TriggerPeriod.Second => previousTick.Date != tick.Date || + previousTick.Hour != tick.Hour || + previousTick.Minute != tick.Minute || + previousTick.Second != tick.Second, - TriggerPeriod.Minute => previousTick.Date != tick.Date || - previousTick.Hour != tick.Hour || - previousTick.Minute != tick.Minute, + TriggerPeriod.Minute => previousTick.Date != tick.Date || + previousTick.Hour != tick.Hour || + previousTick.Minute != tick.Minute, - TriggerPeriod.Hour => previousTick.Date != tick.Date || - previousTick.Hour != tick.Hour, + TriggerPeriod.Hour => previousTick.Date != tick.Date || + previousTick.Hour != tick.Hour, - TriggerPeriod.Day => previousTick.Date != tick.Date, + TriggerPeriod.Day => previousTick.Date != tick.Date, - TriggerPeriod.Month => previousTick.Year != tick.Year || - previousTick.Month != tick.Month, + TriggerPeriod.Month => previousTick.Year != tick.Year || + previousTick.Month != tick.Month, - TriggerPeriod.Year => previousTick.Year != tick.Year, + TriggerPeriod.Year => previousTick.Year != tick.Year, - _ => throw new Exception("Unsupported trigger period."), - }; - } + _ => throw new Exception("Unsupported trigger period."), + }; + } - #endregion + #endregion - #region Series + #region Series - private static void DrawSeries( - SKCanvas canvas, - ZoomInfo zoomInfo, - LineSeries series, - AxisInfo axisInfo) - { - var dataBox = zoomInfo.DataBox; - var data = zoomInfo.Data.Span; + private static void DrawSeries( + SKCanvas canvas, + ZoomInfo zoomInfo, + LineSeries series, + AxisInfo axisInfo) + { + var dataBox = zoomInfo.DataBox; + var data = zoomInfo.Data.Span; - /* get y scale factor */ - var dataRange = axisInfo.Max - axisInfo.Min; - var yScaleFactor = dataBox.Height / dataRange; + /* get y scale factor */ + var dataRange = axisInfo.Max - axisInfo.Min; + var yScaleFactor = dataBox.Height / dataRange; - /* get dx */ - var dx = zoomInfo.IsClippedRight - ? dataBox.Width / data.Length - : dataBox.Width / (data.Length - 1); + /* get dx */ + var dx = zoomInfo.IsClippedRight + ? dataBox.Width / data.Length + : dataBox.Width / (data.Length - 1); - /* draw */ - if (series.Show) - DrawPath(canvas, axisInfo.Min, dataBox, dx, yScaleFactor, data, series.Color); - } + /* draw */ + if (series.Show) + DrawPath(canvas, axisInfo.Min, dataBox, dx, yScaleFactor, data, series.Color); + } - private static void DrawPath( - SKCanvas canvas, - float dataMin, - SKRect dataArea, - float dx, - float yScaleFactor, - Span data, - SKColor color) + private static void DrawPath( + SKCanvas canvas, + float dataMin, + SKRect dataArea, + float dx, + float yScaleFactor, + Span data, + SKColor color) + { + using var strokePaint = new SKPaint { - using var strokePaint = new SKPaint - { - Style = SKPaintStyle.Stroke, - Color = color, - IsAntialias = false /* improves performance */ - }; + Style = SKPaintStyle.Stroke, + Color = color, + IsAntialias = false /* improves performance */ + }; - using var fillPaint = new SKPaint - { - Style = SKPaintStyle.Fill, - Color = new SKColor(color.Red, color.Green, color.Blue, 0x19) - }; + using var fillPaint = new SKPaint + { + Style = SKPaintStyle.Fill, + Color = new SKColor(color.Red, color.Green, color.Blue, 0x19) + }; - var consumed = 0; - var length = data.Length; - var zeroHeight = dataArea.Bottom - (0 - dataMin) * yScaleFactor; + var consumed = 0; + var length = data.Length; + var zeroHeight = dataArea.Bottom - (0 - dataMin) * yScaleFactor; - while (consumed < length) - { - /* create path */ - var stroke_path = new SKPath(); - var fill_path = new SKPath(); - var x = dataArea.Left + dx * consumed; - var y0 = dataArea.Bottom - ((float)data[consumed] - dataMin) * yScaleFactor; + while (consumed < length) + { + /* create path */ + var stroke_path = new SKPath(); + var fill_path = new SKPath(); + var x = dataArea.Left + dx * consumed; + var y0 = dataArea.Bottom - ((float)data[consumed] - dataMin) * yScaleFactor; - stroke_path.MoveTo(x, y0); - fill_path.MoveTo(x, zeroHeight); + stroke_path.MoveTo(x, y0); + fill_path.MoveTo(x, zeroHeight); - for (int i = consumed; i < length; i++) - { - var value = (float)data[i]; + for (int i = consumed; i < length; i++) + { + var value = (float)data[i]; - if (float.IsNaN(value)) // all NaN's in a row will be consumed a few lines later - break; + if (float.IsNaN(value)) // all NaN's in a row will be consumed a few lines later + break; - var y = dataArea.Bottom - (value - dataMin) * yScaleFactor; - x = dataArea.Left + dx * consumed; // do NOT 'currentX += dx' because it constantly accumulates a small error + var y = dataArea.Bottom - (value - dataMin) * yScaleFactor; + x = dataArea.Left + dx * consumed; // do NOT 'currentX += dx' because it constantly accumulates a small error - stroke_path.LineTo(x, y); - fill_path.LineTo(x, y); + stroke_path.LineTo(x, y); + fill_path.LineTo(x, y); - consumed++; - } + consumed++; + } - x = dataArea.Left + dx * consumed - dx; + x = dataArea.Left + dx * consumed - dx; - fill_path.LineTo(x, zeroHeight); - fill_path.Close(); + fill_path.LineTo(x, zeroHeight); + fill_path.Close(); - /* draw path */ - canvas.DrawPath(stroke_path, strokePaint); - canvas.DrawPath(fill_path, fillPaint); + /* draw path */ + canvas.DrawPath(stroke_path, strokePaint); + canvas.DrawPath(fill_path, fillPaint); - /* consume NaNs */ - for (int i = consumed; i < length; i++) - { - var value = (float)data[i]; + /* consume NaNs */ + for (int i = consumed; i < length; i++) + { + var value = (float)data[i]; - if (float.IsNaN(value)) - consumed++; + if (float.IsNaN(value)) + consumed++; - else - break; - } + else + break; } } + } - #endregion - - #region Helpers + #endregion - private static string ToEngineering(double value) - { - if (value == 0) - return "0"; + #region Helpers - if (Math.Abs(value) < 1000) - return value.ToString("G4"); + private static string ToEngineering(double value) + { + if (value == 0) + return "0"; - var exponent = (int)Math.Floor(Math.Log10(Math.Abs(value))); + if (Math.Abs(value) < 1000) + return value.ToString("G4"); - var pattern = (exponent % 3) switch - { - +1 => "##.##e0", - -2 => "##.##e0", - +2 => "###.#e0", - -1 => "###.#e0", - _ => "#.###e0" - }; - - return value.ToString(pattern); - } + var exponent = (int)Math.Floor(Math.Log10(Math.Abs(value))); - private static DateTime RoundUp(DateTime value, TimeSpan roundTo) + var pattern = (exponent % 3) switch { - var modTicks = value.Ticks % roundTo.Ticks; + +1 => "##.##e0", + -2 => "##.##e0", + +2 => "###.#e0", + -1 => "###.#e0", + _ => "#.###e0" + }; + + return value.ToString(pattern); + } - var delta = modTicks == 0 - ? 0 - : roundTo.Ticks - modTicks; + private static DateTime RoundUp(DateTime value, TimeSpan roundTo) + { + var modTicks = value.Ticks % roundTo.Ticks; - return new DateTime(value.Ticks + delta, value.Kind); - } + var delta = modTicks == 0 + ? 0 + : roundTo.Ticks - modTicks; - private static double RoundDown(double number, int decimalPlaces) - { - return Math.Floor(number * Math.Pow(10, decimalPlaces)) / Math.Pow(10, decimalPlaces); - } + return new DateTime(value.Ticks + delta, value.Kind); + } - private static double RoundUp(double number, int decimalPlaces) - { - return Math.Ceiling(number * Math.Pow(10, decimalPlaces)) / Math.Pow(10, decimalPlaces); - } + private static double RoundDown(double number, int decimalPlaces) + { + return Math.Floor(number * Math.Pow(10, decimalPlaces)) / Math.Pow(10, decimalPlaces); + } - #endregion + private static double RoundUp(double number, int decimalPlaces) + { + return Math.Ceiling(number * Math.Pow(10, decimalPlaces)) / Math.Pow(10, decimalPlaces); + } - #region IDisposable + #endregion - public void Dispose() - { - _dotNetHelper?.Dispose(); - } + #region IDisposable - #endregion + public void Dispose() + { + _dotNetHelper?.Dispose(); } + + #endregion } diff --git a/src/Nexus.UI/Charts/ChartTypes.cs b/src/Nexus.UI/Charts/ChartTypes.cs index 487c9bed..16e5c0b8 100644 --- a/src/Nexus.UI/Charts/ChartTypes.cs +++ b/src/Nexus.UI/Charts/ChartTypes.cs @@ -1,78 +1,75 @@ -using Microsoft.AspNetCore.Components; -using Microsoft.AspNetCore.Components.Web; -using SkiaSharp; +using SkiaSharp; -namespace Nexus.UI.Charts -{ - public record AvailabilityData( - DateTime Begin, - DateTime End, - TimeSpan Step, - IReadOnlyList Data - ); +namespace Nexus.UI.Charts; + +public record AvailabilityData( + DateTime Begin, + DateTime End, + TimeSpan Step, + IReadOnlyList Data +); - public record LineSeriesData( - DateTime Begin, - DateTime End, - IList Series - ); +public record LineSeriesData( + DateTime Begin, + DateTime End, + IList Series +); - public record LineSeries( - string Name, - string Unit, - TimeSpan SamplePeriod, - double[] Data) - { - public bool Show { get; set; } = true; - internal string Id { get; } = Guid.NewGuid().ToString(); - internal SKColor Color { get; set; } - } +public record LineSeries( + string Name, + string Unit, + TimeSpan SamplePeriod, + double[] Data) +{ + public bool Show { get; set; } = true; + internal string Id { get; } = Guid.NewGuid().ToString(); + internal SKColor Color { get; set; } +} - internal record struct ZoomInfo( - Memory Data, - SKRect DataBox, - bool IsClippedRight); +internal record struct ZoomInfo( + Memory Data, + SKRect DataBox, + bool IsClippedRight); - internal record struct Position( - float X, - float Y); +internal record struct Position( + float X, + float Y); - internal record AxisInfo( - string Unit, - float OriginalMin, - float OriginalMax) - { - public float Min { get; set; } - public float Max { get; set; } - }; +internal record AxisInfo( + string Unit, + float OriginalMin, + float OriginalMax) +{ + public float Min { get; set; } + public float Max { get; set; } +}; - internal record TimeAxisConfig( +internal record TimeAxisConfig( - /* The tick interval */ - TimeSpan TickInterval, + /* The tick interval */ + TimeSpan TickInterval, - /* The standard tick label format */ - string FastTickLabelFormat, + /* The standard tick label format */ + string FastTickLabelFormat, - /* Ticks where the TriggerPeriod changes will have a slow tick label attached */ - TriggerPeriod SlowTickTrigger, + /* Ticks where the TriggerPeriod changes will have a slow tick label attached */ + TriggerPeriod SlowTickTrigger, - /* The slow tick format (row 1) */ - string? SlowTickLabelFormat1, + /* The slow tick format (row 1) */ + string? SlowTickLabelFormat1, - /* The slow tick format (row 2) */ - string? SlowTickLabelFormat2, + /* The slow tick format (row 2) */ + string? SlowTickLabelFormat2, - /* The cursor label format*/ - string CursorLabelFormat); + /* The cursor label format*/ + string CursorLabelFormat); - internal enum TriggerPeriod - { - Second, - Minute, - Hour, - Day, - Month, - Year - } +internal enum TriggerPeriod +{ + Second, + Minute, + Hour, + Day, + Month, + Year } diff --git a/src/Nexus.UI/Pages/ChartTest.razor.cs b/src/Nexus.UI/Pages/ChartTest.razor.cs index b6553168..186ec081 100644 --- a/src/Nexus.UI/Pages/ChartTest.razor.cs +++ b/src/Nexus.UI/Pages/ChartTest.razor.cs @@ -1,54 +1,53 @@ using Nexus.UI.Charts; -namespace Nexus.UI.Pages +namespace Nexus.UI.Pages; + +public partial class ChartTest { - public partial class ChartTest + private readonly LineSeriesData _lineSeriesData; + + public ChartTest() { - private readonly LineSeriesData _lineSeriesData; + var begin = new DateTime(2020, 01, 01, 0, 0, 0, DateTimeKind.Utc); + var end = new DateTime(2020, 01, 01, 0, 1, 0, DateTimeKind.Utc); + + var random = new Random(); - public ChartTest() + var lineSeries = new LineSeries[] { - var begin = new DateTime(2020, 01, 01, 0, 0, 0, DateTimeKind.Utc); - var end = new DateTime(2020, 01, 01, 0, 1, 0, DateTimeKind.Utc); - - var random = new Random(); - - var lineSeries = new LineSeries[] - { - new LineSeries( - "Wind speed", - "m/s", - TimeSpan.FromMilliseconds(500), - Enumerable.Range(0, 60*2).Select(value => value / 4.0).ToArray()), - - new LineSeries( - "Temperature", - "°C", - TimeSpan.FromSeconds(1), - Enumerable.Range(0, 60).Select(value => random.NextDouble() * 10 - 5).ToArray()), - - new LineSeries( - "Pressure", - "mbar", - TimeSpan.FromSeconds(1), - Enumerable.Range(0, 60).Select(value => random.NextDouble() * 100 + 1000).ToArray()) - }; - - lineSeries[0].Data[0] = double.NaN; - - lineSeries[0].Data[5] = double.NaN; - lineSeries[0].Data[6] = double.NaN; - - lineSeries[0].Data[10] = double.NaN; - lineSeries[0].Data[11] = double.NaN; - lineSeries[0].Data[12] = double.NaN; - - lineSeries[0].Data[15] = double.NaN; - lineSeries[0].Data[16] = double.NaN; - lineSeries[0].Data[17] = double.NaN; - lineSeries[0].Data[18] = double.NaN; - - _lineSeriesData = new LineSeriesData(begin, end, lineSeries); - } + new LineSeries( + "Wind speed", + "m/s", + TimeSpan.FromMilliseconds(500), + Enumerable.Range(0, 60*2).Select(value => value / 4.0).ToArray()), + + new LineSeries( + "Temperature", + "°C", + TimeSpan.FromSeconds(1), + Enumerable.Range(0, 60).Select(value => random.NextDouble() * 10 - 5).ToArray()), + + new LineSeries( + "Pressure", + "mbar", + TimeSpan.FromSeconds(1), + Enumerable.Range(0, 60).Select(value => random.NextDouble() * 100 + 1000).ToArray()) + }; + + lineSeries[0].Data[0] = double.NaN; + + lineSeries[0].Data[5] = double.NaN; + lineSeries[0].Data[6] = double.NaN; + + lineSeries[0].Data[10] = double.NaN; + lineSeries[0].Data[11] = double.NaN; + lineSeries[0].Data[12] = double.NaN; + + lineSeries[0].Data[15] = double.NaN; + lineSeries[0].Data[16] = double.NaN; + lineSeries[0].Data[17] = double.NaN; + lineSeries[0].Data[18] = double.NaN; + + _lineSeriesData = new LineSeriesData(begin, end, lineSeries); } } diff --git a/src/Nexus.UI/Services/NexusAuthenticationStateProvider.cs b/src/Nexus.UI/Services/NexusAuthenticationStateProvider.cs index 0a1fb941..ef11c96c 100644 --- a/src/Nexus.UI/Services/NexusAuthenticationStateProvider.cs +++ b/src/Nexus.UI/Services/NexusAuthenticationStateProvider.cs @@ -2,50 +2,49 @@ using Nexus.Api; using System.Security.Claims; -namespace Nexus.UI.Services +namespace Nexus.UI.Services; + +public class NexusAuthenticationStateProvider : AuthenticationStateProvider { - public class NexusAuthenticationStateProvider : AuthenticationStateProvider + private readonly INexusClient _client; + + public NexusAuthenticationStateProvider(INexusClient client) { - private readonly INexusClient _client; + _client = client; + } - public NexusAuthenticationStateProvider(INexusClient client) - { - _client = client; - } + public override async Task GetAuthenticationStateAsync() + { + ClaimsIdentity identity; - public override async Task GetAuthenticationStateAsync() - { - ClaimsIdentity identity; + const string NAME_CLAIM = "name"; + const string ROLE_CLAIM = "role"; - const string NAME_CLAIM = "name"; - const string ROLE_CLAIM = "role"; + try + { + var meResponse = await _client.Users.GetMeAsync(); - try - { - var meResponse = await _client.Users.GetMeAsync(); - - var claims = new List - { - new Claim(NAME_CLAIM, meResponse.User.Name) - }; - - if (meResponse.IsAdmin) - claims.Add(new Claim(ROLE_CLAIM, "Administrator")); - - identity = new ClaimsIdentity( - claims, - authenticationType: meResponse.UserId.Split(new[] { '@' }, count: 2)[1], - nameType: NAME_CLAIM, - roleType: ROLE_CLAIM); - } - catch (Exception) + var claims = new List { - identity = new ClaimsIdentity(); - } + new Claim(NAME_CLAIM, meResponse.User.Name) + }; - var principal = new ClaimsPrincipal(identity); + if (meResponse.IsAdmin) + claims.Add(new Claim(ROLE_CLAIM, "Administrator")); - return new AuthenticationState(principal); + identity = new ClaimsIdentity( + claims, + authenticationType: meResponse.UserId.Split(new[] { '@' }, count: 2)[1], + nameType: NAME_CLAIM, + roleType: ROLE_CLAIM); + } + catch (Exception) + { + identity = new ClaimsIdentity(); } + + var principal = new ClaimsPrincipal(identity); + + return new AuthenticationState(principal); } } diff --git a/src/Nexus.UI/Services/TypeFaceService.cs b/src/Nexus.UI/Services/TypeFaceService.cs index 8297c943..9144d19d 100644 --- a/src/Nexus.UI/Services/TypeFaceService.cs +++ b/src/Nexus.UI/Services/TypeFaceService.cs @@ -1,52 +1,51 @@ using SkiaSharp; using System.Reflection; -namespace Nexus.UI.Services +namespace Nexus.UI.Services; + +// https://github.com/mono/SkiaSharp/issues/1902 +// https://fontsgeek.com/fonts/Courier-New-Regular + +public class TypeFaceService { - // https://github.com/mono/SkiaSharp/issues/1902 - // https://fontsgeek.com/fonts/Courier-New-Regular + private readonly Dictionary _typeFaces = new(); - public class TypeFaceService + public SKTypeface GetTTF(string ttfName) { - private readonly Dictionary _typeFaces = new(); + if (_typeFaces.ContainsKey(ttfName)) + return _typeFaces[ttfName]; - public SKTypeface GetTTF(string ttfName) - { - if (_typeFaces.ContainsKey(ttfName)) - return _typeFaces[ttfName]; + else if (LoadTypeFace(ttfName)) + return _typeFaces[ttfName]; - else if (LoadTypeFace(ttfName)) - return _typeFaces[ttfName]; + return SKTypeface.Default; + } - return SKTypeface.Default; - } + private bool LoadTypeFace(string ttfName) + { + var assembly = Assembly.GetExecutingAssembly(); - private bool LoadTypeFace(string ttfName) + try { - var assembly = Assembly.GetExecutingAssembly(); - - try + var fileName = ttfName.ToLower() + ".ttf"; + foreach (var item in assembly.GetManifestResourceNames()) { - var fileName = ttfName.ToLower() + ".ttf"; - foreach (var item in assembly.GetManifestResourceNames()) + if (item.ToLower().EndsWith(fileName)) { - if (item.ToLower().EndsWith(fileName)) - { - var stream = assembly.GetManifestResourceStream(item); - var typeFace = SKTypeface.FromStream(stream); + var stream = assembly.GetManifestResourceStream(item); + var typeFace = SKTypeface.FromStream(stream); - _typeFaces.Add(ttfName, typeFace); + _typeFaces.Add(ttfName, typeFace); - return true; - } + return true; } } - catch - { - /* missing resource */ - } - - return false; } + catch + { + /* missing resource */ + } + + return false; } } \ No newline at end of file diff --git a/src/Nexus/API/ArtifactsController.cs b/src/Nexus/API/ArtifactsController.cs index 9954180a..aefaada7 100644 --- a/src/Nexus/API/ArtifactsController.cs +++ b/src/Nexus/API/ArtifactsController.cs @@ -2,58 +2,57 @@ using Microsoft.AspNetCore.Mvc; using Nexus.Services; -namespace Nexus.Controllers +namespace Nexus.Controllers; + +/// +/// Provides access to artifacts. +/// +[Authorize] +[ApiController] +[ApiVersion("1.0")] +[Route("api/v{version:apiVersion}/[controller]")] +internal class ArtifactsController : ControllerBase { - /// - /// Provides access to artifacts. - /// - [Authorize] - [ApiController] - [ApiVersion("1.0")] - [Route("api/v{version:apiVersion}/[controller]")] - internal class ArtifactsController : ControllerBase - { - // GET /api/artifacts/{artifactId} + // GET /api/artifacts/{artifactId} - #region Fields + #region Fields - public IDatabaseService _databaseService; + public IDatabaseService _databaseService; - #endregion + #endregion - #region Constructors + #region Constructors - public ArtifactsController( - IDatabaseService databaseService) - { - _databaseService = databaseService; - } + public ArtifactsController( + IDatabaseService databaseService) + { + _databaseService = databaseService; + } - #endregion + #endregion - #region Methods + #region Methods - /// - /// Gets the specified artifact. - /// - /// The artifact identifier. - [HttpGet("{artifactId}")] - public ActionResult - Download( - string artifactId) + /// + /// Gets the specified artifact. + /// + /// The artifact identifier. + [HttpGet("{artifactId}")] + public ActionResult + Download( + string artifactId) + { + if (_databaseService.TryReadArtifact(artifactId, out var artifactStream)) { - if (_databaseService.TryReadArtifact(artifactId, out var artifactStream)) - { - Response.Headers.ContentLength = artifactStream.Length; - return File(artifactStream, "application/octet-stream"); // do not set filname here, otherwise will not work! - } - - else - { - return NotFound($"Could not find artifact {artifactId}."); - } + Response.Headers.ContentLength = artifactStream.Length; + return File(artifactStream, "application/octet-stream"); // do not set filname here, otherwise will not work! } - #endregion + else + { + return NotFound($"Could not find artifact {artifactId}."); + } } + + #endregion } diff --git a/src/Nexus/API/CatalogsController.cs b/src/Nexus/API/CatalogsController.cs index 271f3419..6ac2239d 100644 --- a/src/Nexus/API/CatalogsController.cs +++ b/src/Nexus/API/CatalogsController.cs @@ -13,571 +13,570 @@ using System.Text.RegularExpressions; using static OpenIddict.Abstractions.OpenIddictConstants; -namespace Nexus.Controllers +namespace Nexus.Controllers; + +/// +/// Provides access to catalogs. +/// +[Authorize] +[ApiController] +[ApiVersion("1.0")] +[Route("api/v{version:apiVersion}/[controller]")] +internal class CatalogsController : ControllerBase { - /// - /// Provides access to catalogs. - /// - [Authorize] - [ApiController] - [ApiVersion("1.0")] - [Route("api/v{version:apiVersion}/[controller]")] - internal class CatalogsController : ControllerBase + // POST /api/catalogs/search-items + // GET /api/catalogs/{catalogId} + // GET /api/catalogs/{catalogId}/child-catalog-infos + // GET /api/catalogs/{catalogId}/timerange + // GET /api/catalogs/{catalogId}/availability + // GET /api/catalogs/{catalogId}/license + // GET /api/catalogs/{catalogId}/attachments + // PUT /api/catalogs/{catalogId}/attachments + // DELETE /api/catalogs/{catalogId}/attachments/{attachmentId} + // GET /api/catalogs/{catalogId}/attachments/{attachmentId}/content + + // GET /api/catalogs/{catalogId}/metadata + // PUT /api/catalogs/{catalogId}/metadata + + #region Fields + + private readonly AppState _appState; + private readonly IDatabaseService _databaseService; + private readonly IDataControllerService _dataControllerService; + + #endregion + + #region Constructors + + public CatalogsController( + AppState appState, + IDatabaseService databaseService, + IDataControllerService dataControllerService) { - // POST /api/catalogs/search-items - // GET /api/catalogs/{catalogId} - // GET /api/catalogs/{catalogId}/child-catalog-infos - // GET /api/catalogs/{catalogId}/timerange - // GET /api/catalogs/{catalogId}/availability - // GET /api/catalogs/{catalogId}/license - // GET /api/catalogs/{catalogId}/attachments - // PUT /api/catalogs/{catalogId}/attachments - // DELETE /api/catalogs/{catalogId}/attachments/{attachmentId} - // GET /api/catalogs/{catalogId}/attachments/{attachmentId}/content - - // GET /api/catalogs/{catalogId}/metadata - // PUT /api/catalogs/{catalogId}/metadata - - #region Fields - - private readonly AppState _appState; - private readonly IDatabaseService _databaseService; - private readonly IDataControllerService _dataControllerService; - - #endregion - - #region Constructors - - public CatalogsController( - AppState appState, - IDatabaseService databaseService, - IDataControllerService dataControllerService) - { - _appState = appState; - _databaseService = databaseService; - _dataControllerService = dataControllerService; - } + _appState = appState; + _databaseService = databaseService; + _dataControllerService = dataControllerService; + } - #endregion + #endregion - #region Methods + #region Methods - /// - /// Searches for the given resource paths and returns the corresponding catalog items. - /// - /// The list of resource paths. - /// A token to cancel the current operation. - [HttpPost("search-items")] - public async Task>> - SearchCatalogItemsAsync( - [FromBody] string[] resourcePaths, - CancellationToken cancellationToken) - { - var root = _appState.CatalogState.Root; + /// + /// Searches for the given resource paths and returns the corresponding catalog items. + /// + /// The list of resource paths. + /// A token to cancel the current operation. + [HttpPost("search-items")] + public async Task>> + SearchCatalogItemsAsync( + [FromBody] string[] resourcePaths, + CancellationToken cancellationToken) + { + var root = _appState.CatalogState.Root; - // translate resource paths to catalog item requests - (string ResourcePath, CatalogItemRequest Request)[] resourcePathAndRequests; + // translate resource paths to catalog item requests + (string ResourcePath, CatalogItemRequest Request)[] resourcePathAndRequests; - try + try + { + resourcePathAndRequests = await Task.WhenAll(resourcePaths.Distinct().Select(async resourcePath => { - resourcePathAndRequests = await Task.WhenAll(resourcePaths.Distinct().Select(async resourcePath => - { - var catalogItemRequest = await root - .TryFindAsync(resourcePath, cancellationToken) - ?? throw new ValidationException($"Could not find resource path {resourcePath}."); + var catalogItemRequest = await root + .TryFindAsync(resourcePath, cancellationToken) + ?? throw new ValidationException($"Could not find resource path {resourcePath}."); - return (resourcePath, catalogItemRequest); - })); - } - catch (ValidationException ex) - { - return UnprocessableEntity(ex.Message); - } + return (resourcePath, catalogItemRequest); + })); + } + catch (ValidationException ex) + { + return UnprocessableEntity(ex.Message); + } - // authorize - try + // authorize + try + { + foreach (var group in resourcePathAndRequests.GroupBy(current => current.Request.Container.Id)) { - foreach (var group in resourcePathAndRequests.GroupBy(current => current.Request.Container.Id)) - { - var catalogContainer = group.First().Request.Container; + var catalogContainer = group.First().Request.Container; - if (!AuthUtilities.IsCatalogReadable(catalogContainer.Id, catalogContainer.Metadata, catalogContainer.Owner, User)) - throw new UnauthorizedAccessException($"The current user is not permitted to access catalog {catalogContainer.Id}."); - } + if (!AuthUtilities.IsCatalogReadable(catalogContainer.Id, catalogContainer.Metadata, catalogContainer.Owner, User)) + throw new UnauthorizedAccessException($"The current user is not permitted to access catalog {catalogContainer.Id}."); } - catch (UnauthorizedAccessException ex) - { - return StatusCode(StatusCodes.Status403Forbidden, ex.Message); - } - - var response = resourcePathAndRequests - .ToDictionary(item => item.ResourcePath, item => item.Request.Item); - - return response; } - - /// - /// Gets the specified catalog. - /// - /// The catalog identifier. - /// A token to cancel the current operation. - [HttpGet("{catalogId}")] - public Task> - GetAsync( - string catalogId, - CancellationToken cancellationToken) + catch (UnauthorizedAccessException ex) { - catalogId = WebUtility.UrlDecode(catalogId); - - var response = ProtectCatalogAsync(catalogId, ensureReadable: true, ensureWritable: false, async catalogContainer => - { - var lazyCatalogInfo = await catalogContainer.GetLazyCatalogInfoAsync(cancellationToken); - var catalog = lazyCatalogInfo.Catalog; + return StatusCode(StatusCodes.Status403Forbidden, ex.Message); + } - return catalog; - }, cancellationToken); + var response = resourcePathAndRequests + .ToDictionary(item => item.ResourcePath, item => item.Request.Item); - return response; - } + return response; + } - /// - /// Gets a list of child catalog info for the provided parent catalog identifier. - /// - /// The parent catalog identifier. - /// A token to cancel the current operation. - [HttpGet("{catalogId}/child-catalog-infos")] - public async Task> - GetChildCatalogInfosAsync( + /// + /// Gets the specified catalog. + /// + /// The catalog identifier. + /// A token to cancel the current operation. + [HttpGet("{catalogId}")] + public Task> + GetAsync( string catalogId, CancellationToken cancellationToken) + { + catalogId = WebUtility.UrlDecode(catalogId); + + var response = ProtectCatalogAsync(catalogId, ensureReadable: true, ensureWritable: false, async catalogContainer => { - catalogId = WebUtility.UrlDecode(catalogId); + var lazyCatalogInfo = await catalogContainer.GetLazyCatalogInfoAsync(cancellationToken); + var catalog = lazyCatalogInfo.Catalog; - var response = await ProtectCatalogAsync(catalogId, ensureReadable: false, ensureWritable: false, async catalogContainer => - { - var childContainers = await catalogContainer.GetChildCatalogContainersAsync(cancellationToken); + return catalog; + }, cancellationToken); - return childContainers - .Select(childContainer => - { - // TODO: Create CatalogInfo along with CatalogContainer to improve performance and reduce GC pressure? - - var id = childContainer.Id; - var title = childContainer.Title; - var contact = childContainer.Metadata.Contact; - - string? readme = default; - - if (_databaseService.TryReadAttachment(childContainer.Id, "README.md", out var readmeStream)) - { - using var reader = new StreamReader(readmeStream); - readme = reader.ReadToEnd(); - } - - string? license = default; - - if (_databaseService.TryReadAttachment(childContainer.Id, "LICENSE.md", out var licenseStream)) - { - using var reader = new StreamReader(licenseStream); - license = reader.ReadToEnd(); - } - - var isReadable = AuthUtilities.IsCatalogReadable(childContainer.Id, childContainer.Metadata, childContainer.Owner, User); - var isWritable = AuthUtilities.IsCatalogWritable(childContainer.Id, childContainer.Metadata, User); - - var isReleased = childContainer.Owner is null || - childContainer.IsReleasable && Regex.IsMatch(id, childContainer.DataSourceRegistration.ReleasePattern ?? ""); - - var isVisible = - isReadable || Regex.IsMatch(id, childContainer.DataSourceRegistration.VisibilityPattern ?? ""); - - var isOwner = childContainer.Owner?.FindFirstValue(Claims.Subject) == User.FindFirstValue(Claims.Subject); - - return new CatalogInfo( - id, - title, - contact, - readme, - license, - isReadable, - isWritable, - isReleased, - isVisible, - isOwner, - childContainer.DataSourceRegistration.InfoUrl, - childContainer.DataSourceRegistration.Type, - childContainer.DataSourceRegistration.Id, - childContainer.PackageReference.Id - ); - }) - .ToArray(); - }, cancellationToken); - - return response; - } + return response; + } - /// - /// Gets the specified catalog's time range. - /// - /// The catalog identifier. - /// A token to cancel the current operation. - [HttpGet("{catalogId}/timerange")] - public Task> - GetTimeRangeAsync( - string catalogId, - CancellationToken cancellationToken) + /// + /// Gets a list of child catalog info for the provided parent catalog identifier. + /// + /// The parent catalog identifier. + /// A token to cancel the current operation. + [HttpGet("{catalogId}/child-catalog-infos")] + public async Task> + GetChildCatalogInfosAsync( + string catalogId, + CancellationToken cancellationToken) + { + catalogId = WebUtility.UrlDecode(catalogId); + + var response = await ProtectCatalogAsync(catalogId, ensureReadable: false, ensureWritable: false, async catalogContainer => { - catalogId = WebUtility.UrlDecode(catalogId); + var childContainers = await catalogContainer.GetChildCatalogContainersAsync(cancellationToken); - var response = ProtectCatalogAsync(catalogId, ensureReadable: true, ensureWritable: false, async catalogContainer => - { - using var dataSource = await _dataControllerService.GetDataSourceControllerAsync(catalogContainer.DataSourceRegistration, cancellationToken); - return await dataSource.GetTimeRangeAsync(catalogContainer.Id, cancellationToken); - }, cancellationToken); + return childContainers + .Select(childContainer => + { + // TODO: Create CatalogInfo along with CatalogContainer to improve performance and reduce GC pressure? - return response; - } + var id = childContainer.Id; + var title = childContainer.Title; + var contact = childContainer.Metadata.Contact; - /// - /// Gets the specified catalog's availability. - /// - /// The catalog identifier. - /// Start date/time. - /// End date/time. - /// Step period. - /// A token to cancel the current operation. - [HttpGet("{catalogId}/availability")] - public async Task> - GetAvailabilityAsync( - string catalogId, - [BindRequired] DateTime begin, - [BindRequired] DateTime end, - [BindRequired] TimeSpan step, - CancellationToken cancellationToken) - { - catalogId = WebUtility.UrlDecode(catalogId); - begin = begin.ToUniversalTime(); - end = end.ToUniversalTime(); + string? readme = default; - if (begin >= end) - return UnprocessableEntity("The end date/time must be before the begin date/time."); + if (_databaseService.TryReadAttachment(childContainer.Id, "README.md", out var readmeStream)) + { + using var reader = new StreamReader(readmeStream); + readme = reader.ReadToEnd(); + } - if (step <= TimeSpan.Zero) - return UnprocessableEntity("The step must be > 0."); + string? license = default; - if ((end - begin).Ticks / step.Ticks > 1000) - return UnprocessableEntity("The number of steps is too large."); + if (_databaseService.TryReadAttachment(childContainer.Id, "LICENSE.md", out var licenseStream)) + { + using var reader = new StreamReader(licenseStream); + license = reader.ReadToEnd(); + } - var response = await ProtectCatalogAsync(catalogId, ensureReadable: true, ensureWritable: false, async catalogContainer => - { - using var dataSource = await _dataControllerService.GetDataSourceControllerAsync(catalogContainer.DataSourceRegistration, cancellationToken); - return await dataSource.GetAvailabilityAsync(catalogContainer.Id, begin, end, step, cancellationToken); - }, cancellationToken); + var isReadable = AuthUtilities.IsCatalogReadable(childContainer.Id, childContainer.Metadata, childContainer.Owner, User); + var isWritable = AuthUtilities.IsCatalogWritable(childContainer.Id, childContainer.Metadata, User); + + var isReleased = childContainer.Owner is null || + childContainer.IsReleasable && Regex.IsMatch(id, childContainer.DataSourceRegistration.ReleasePattern ?? ""); + + var isVisible = + isReadable || Regex.IsMatch(id, childContainer.DataSourceRegistration.VisibilityPattern ?? ""); + + var isOwner = childContainer.Owner?.FindFirstValue(Claims.Subject) == User.FindFirstValue(Claims.Subject); + + return new CatalogInfo( + id, + title, + contact, + readme, + license, + isReadable, + isWritable, + isReleased, + isVisible, + isOwner, + childContainer.DataSourceRegistration.InfoUrl, + childContainer.DataSourceRegistration.Type, + childContainer.DataSourceRegistration.Id, + childContainer.PackageReference.Id + ); + }) + .ToArray(); + }, cancellationToken); + + return response; + } - return response; - } + /// + /// Gets the specified catalog's time range. + /// + /// The catalog identifier. + /// A token to cancel the current operation. + [HttpGet("{catalogId}/timerange")] + public Task> + GetTimeRangeAsync( + string catalogId, + CancellationToken cancellationToken) + { + catalogId = WebUtility.UrlDecode(catalogId); - /// - /// Gets the license of the catalog if available. - /// - /// The catalog identifier. - /// A token to cancel the current operation. - [HttpGet("{catalogId}/license")] - [return: CanBeNull] - public async Task> - GetLicenseAsync( - string catalogId, - CancellationToken cancellationToken) + var response = ProtectCatalogAsync(catalogId, ensureReadable: true, ensureWritable: false, async catalogContainer => { - catalogId = WebUtility.UrlDecode(catalogId); + using var dataSource = await _dataControllerService.GetDataSourceControllerAsync(catalogContainer.DataSourceRegistration, cancellationToken); + return await dataSource.GetTimeRangeAsync(catalogContainer.Id, cancellationToken); + }, cancellationToken); - var response = await ProtectCatalogAsync(catalogId, ensureReadable: false, ensureWritable: false, async catalogContainer => - { - string? license = default; + return response; + } - if (_databaseService.TryReadAttachment(catalogContainer.Id, "LICENSE.md", out var licenseStream)) - { - using var reader = new StreamReader(licenseStream); - license = await reader.ReadToEndAsync(); - } + /// + /// Gets the specified catalog's availability. + /// + /// The catalog identifier. + /// Start date/time. + /// End date/time. + /// Step period. + /// A token to cancel the current operation. + [HttpGet("{catalogId}/availability")] + public async Task> + GetAvailabilityAsync( + string catalogId, + [BindRequired] DateTime begin, + [BindRequired] DateTime end, + [BindRequired] TimeSpan step, + CancellationToken cancellationToken) + { + catalogId = WebUtility.UrlDecode(catalogId); + begin = begin.ToUniversalTime(); + end = end.ToUniversalTime(); - if (license is null) - { - var catalogInfo = await catalogContainer.GetLazyCatalogInfoAsync(cancellationToken); - license = catalogInfo.Catalog.Properties?.GetStringValue(DataModelExtensions.LicenseKey); - } + if (begin >= end) + return UnprocessableEntity("The end date/time must be before the begin date/time."); - return license; - }, cancellationToken); + if (step <= TimeSpan.Zero) + return UnprocessableEntity("The step must be > 0."); - return response; - } + if ((end - begin).Ticks / step.Ticks > 1000) + return UnprocessableEntity("The number of steps is too large."); - /// - /// Gets all attachments for the specified catalog. - /// - /// The catalog identifier. - /// A token to cancel the current operation. - [HttpGet("{catalogId}/attachments")] - public Task> - GetAttachmentsAsync( - string catalogId, - CancellationToken cancellationToken) + var response = await ProtectCatalogAsync(catalogId, ensureReadable: true, ensureWritable: false, async catalogContainer => { - catalogId = WebUtility.UrlDecode(catalogId); + using var dataSource = await _dataControllerService.GetDataSourceControllerAsync(catalogContainer.DataSourceRegistration, cancellationToken); + return await dataSource.GetAvailabilityAsync(catalogContainer.Id, begin, end, step, cancellationToken); + }, cancellationToken); - var response = ProtectCatalogAsync(catalogId, ensureReadable: true, ensureWritable: false, catalog => - { - return Task.FromResult>(_databaseService.EnumerateAttachments(catalogId).ToArray()); - }, cancellationToken); + return response; + } - return response; - } + /// + /// Gets the license of the catalog if available. + /// + /// The catalog identifier. + /// A token to cancel the current operation. + [HttpGet("{catalogId}/license")] + [return: CanBeNull] + public async Task> + GetLicenseAsync( + string catalogId, + CancellationToken cancellationToken) + { + catalogId = WebUtility.UrlDecode(catalogId); - /// - /// Uploads the specified attachment. - /// - /// The catalog identifier. - /// The attachment identifier. - /// The binary file content. - /// A token to cancel the current operation. - [HttpPut("{catalogId}/attachments/{attachmentId}")] - [DisableRequestSizeLimit] - public Task - UploadAttachmentAsync( - string catalogId, - string attachmentId, - [FromBody] Stream content, - CancellationToken cancellationToken) + var response = await ProtectCatalogAsync(catalogId, ensureReadable: false, ensureWritable: false, async catalogContainer => { - catalogId = WebUtility.UrlDecode(catalogId); - attachmentId = WebUtility.UrlDecode(attachmentId); + string? license = default; - var response = ProtectCatalogNonGenericAsync(catalogId, ensureReadable: false, ensureWritable: true, async catalog => + if (_databaseService.TryReadAttachment(catalogContainer.Id, "LICENSE.md", out var licenseStream)) { - try - { - using var attachmentStream = _databaseService.WriteAttachment(catalogId, attachmentId); - await content.CopyToAsync(attachmentStream, cancellationToken); + using var reader = new StreamReader(licenseStream); + license = await reader.ReadToEndAsync(); + } - return Ok(); - } - catch (IOException ex) - { - return StatusCode(StatusCodes.Status423Locked, ex.Message); - } - catch (Exception) - { - try - { - if (_databaseService.AttachmentExists(catalogId, attachmentId)) - _databaseService.DeleteAttachment(catalogId, attachmentId); - } - catch (Exception) - { - // - } + if (license is null) + { + var catalogInfo = await catalogContainer.GetLazyCatalogInfoAsync(cancellationToken); + license = catalogInfo.Catalog.Properties?.GetStringValue(DataModelExtensions.LicenseKey); + } - throw; - } - }, cancellationToken); + return license; + }, cancellationToken); - return response; - } + return response; + } - /// - /// Deletes the specified attachment. - /// - /// The catalog identifier. - /// The attachment identifier. - /// A token to cancel the current operation. - [HttpDelete("{catalogId}/attachments/{attachmentId}")] - public Task - DeleteAttachmentAsync( - string catalogId, - string attachmentId, - CancellationToken cancellationToken) + /// + /// Gets all attachments for the specified catalog. + /// + /// The catalog identifier. + /// A token to cancel the current operation. + [HttpGet("{catalogId}/attachments")] + public Task> + GetAttachmentsAsync( + string catalogId, + CancellationToken cancellationToken) + { + catalogId = WebUtility.UrlDecode(catalogId); + + var response = ProtectCatalogAsync(catalogId, ensureReadable: true, ensureWritable: false, catalog => { - catalogId = WebUtility.UrlDecode(catalogId); - attachmentId = WebUtility.UrlDecode(attachmentId); + return Task.FromResult>(_databaseService.EnumerateAttachments(catalogId).ToArray()); + }, cancellationToken); - var response = ProtectCatalogNonGenericAsync(catalogId, ensureReadable: false, ensureWritable: true, catalog => - { - try - { - _databaseService.DeleteAttachment(catalogId, attachmentId); - return Task.FromResult( - Ok()); - } - catch (IOException ex) - { - return Task.FromResult( - StatusCode(StatusCodes.Status423Locked, ex.Message)); - } - }, cancellationToken); + return response; + } - return response; - } + /// + /// Uploads the specified attachment. + /// + /// The catalog identifier. + /// The attachment identifier. + /// The binary file content. + /// A token to cancel the current operation. + [HttpPut("{catalogId}/attachments/{attachmentId}")] + [DisableRequestSizeLimit] + public Task + UploadAttachmentAsync( + string catalogId, + string attachmentId, + [FromBody] Stream content, + CancellationToken cancellationToken) + { + catalogId = WebUtility.UrlDecode(catalogId); + attachmentId = WebUtility.UrlDecode(attachmentId); - /// - /// Gets the specified attachment. - /// - /// The catalog identifier. - /// The attachment identifier. - /// A token to cancel the current operation. - [HttpGet("{catalogId}/attachments/{attachmentId}/content")] - public Task - GetAttachmentStreamAsync( - string catalogId, - string attachmentId, - CancellationToken cancellationToken) + var response = ProtectCatalogNonGenericAsync(catalogId, ensureReadable: false, ensureWritable: true, async catalog => { - catalogId = WebUtility.UrlDecode(catalogId); - attachmentId = WebUtility.UrlDecode(attachmentId); + try + { + using var attachmentStream = _databaseService.WriteAttachment(catalogId, attachmentId); + await content.CopyToAsync(attachmentStream, cancellationToken); - var response = ProtectCatalogNonGenericAsync(catalogId, ensureReadable: true, ensureWritable: false, catalog => + return Ok(); + } + catch (IOException ex) + { + return StatusCode(StatusCodes.Status423Locked, ex.Message); + } + catch (Exception) { try { - if (_databaseService.TryReadAttachment(catalogId, attachmentId, out var attachmentStream)) - { - Response.Headers.ContentLength = attachmentStream.Length; - return Task.FromResult( - File(attachmentStream, "application/octet-stream", attachmentId)); - } - else - { - return Task.FromResult( - NotFound($"Could not find attachment {attachmentId} for catalog {catalogId}.")); - } + if (_databaseService.AttachmentExists(catalogId, attachmentId)) + _databaseService.DeleteAttachment(catalogId, attachmentId); } - catch (IOException ex) + catch (Exception) { - return Task.FromResult( - StatusCode(StatusCodes.Status423Locked, ex.Message)); + // } - }, cancellationToken); - - return response; - } - /// - /// Gets the catalog metadata. - /// - /// The catalog identifier. - /// A token to cancel the current operation. - [HttpGet("{catalogId}/metadata")] - public Task> - GetMetadataAsync( - string catalogId, - CancellationToken cancellationToken) - { - catalogId = WebUtility.UrlDecode(catalogId); + throw; + } + }, cancellationToken); - var response = ProtectCatalogAsync(catalogId, ensureReadable: true, ensureWritable: false, async catalogContainer => - { - return await Task.FromResult(catalogContainer.Metadata); - }, cancellationToken); + return response; + } - return response; - } + /// + /// Deletes the specified attachment. + /// + /// The catalog identifier. + /// The attachment identifier. + /// A token to cancel the current operation. + [HttpDelete("{catalogId}/attachments/{attachmentId}")] + public Task + DeleteAttachmentAsync( + string catalogId, + string attachmentId, + CancellationToken cancellationToken) + { + catalogId = WebUtility.UrlDecode(catalogId); + attachmentId = WebUtility.UrlDecode(attachmentId); - /// - /// Puts the catalog metadata. - /// - /// The catalog identifier. - /// The catalog metadata to set. - /// A token to cancel the current operation. - [HttpPut("{catalogId}/metadata")] - public Task - SetMetadataAsync( - string catalogId, - [FromBody] CatalogMetadata metadata, - CancellationToken cancellationToken) + var response = ProtectCatalogNonGenericAsync(catalogId, ensureReadable: false, ensureWritable: true, catalog => { - catalogId = WebUtility.UrlDecode(catalogId); - - var response = ProtectCatalogAsync(catalogId, ensureReadable: true, ensureWritable: false, async catalogContainer => + try { - var canEdit = AuthUtilities.IsCatalogWritable(catalogId, catalogContainer.Metadata, User); - - if (!canEdit) - return StatusCode(StatusCodes.Status403Forbidden, $"The current user is not permitted to modify the catalog {catalogId}."); - - await catalogContainer.UpdateMetadataAsync(metadata); - - return new object(); - - }, cancellationToken); + _databaseService.DeleteAttachment(catalogId, attachmentId); + return Task.FromResult( + Ok()); + } + catch (IOException ex) + { + return Task.FromResult( + StatusCode(StatusCodes.Status423Locked, ex.Message)); + } + }, cancellationToken); - return response; - } + return response; + } - private async Task> ProtectCatalogAsync( + /// + /// Gets the specified attachment. + /// + /// The catalog identifier. + /// The attachment identifier. + /// A token to cancel the current operation. + [HttpGet("{catalogId}/attachments/{attachmentId}/content")] + public Task + GetAttachmentStreamAsync( string catalogId, - bool ensureReadable, - bool ensureWritable, - Func>> action, + string attachmentId, CancellationToken cancellationToken) - { - var root = _appState.CatalogState.Root; - - var catalogContainer = catalogId == CatalogContainer.RootCatalogId - ? root - : await root.TryFindCatalogContainerAsync(catalogId, cancellationToken); + { + catalogId = WebUtility.UrlDecode(catalogId); + attachmentId = WebUtility.UrlDecode(attachmentId); - if (catalogContainer is not null) + var response = ProtectCatalogNonGenericAsync(catalogId, ensureReadable: true, ensureWritable: false, catalog => + { + try { - if (ensureReadable && !AuthUtilities.IsCatalogReadable( - catalogContainer.Id, catalogContainer.Metadata, catalogContainer.Owner, User)) + if (_databaseService.TryReadAttachment(catalogId, attachmentId, out var attachmentStream)) { - return StatusCode( - StatusCodes.Status403Forbidden, - $"The current user is not permitted to read the catalog {catalogId}."); + Response.Headers.ContentLength = attachmentStream.Length; + return Task.FromResult( + File(attachmentStream, "application/octet-stream", attachmentId)); } - - if (ensureWritable && !AuthUtilities.IsCatalogWritable( - catalogContainer.Id, catalogContainer.Metadata, User)) + else { - return StatusCode( - StatusCodes.Status403Forbidden, - $"The current user is not permitted to modify the catalog {catalogId}."); + return Task.FromResult( + NotFound($"Could not find attachment {attachmentId} for catalog {catalogId}.")); } - - return await action.Invoke(catalogContainer); } - else + catch (IOException ex) { - return NotFound(catalogId); + return Task.FromResult( + StatusCode(StatusCodes.Status423Locked, ex.Message)); } - } + }, cancellationToken); + + return response; + } - private async Task ProtectCatalogNonGenericAsync( + /// + /// Gets the catalog metadata. + /// + /// The catalog identifier. + /// A token to cancel the current operation. + [HttpGet("{catalogId}/metadata")] + public Task> + GetMetadataAsync( string catalogId, - bool ensureReadable, - bool ensureWritable, - Func> action, CancellationToken cancellationToken) + { + catalogId = WebUtility.UrlDecode(catalogId); + + var response = ProtectCatalogAsync(catalogId, ensureReadable: true, ensureWritable: false, async catalogContainer => { - var root = _appState.CatalogState.Root; - var catalogContainer = await root.TryFindCatalogContainerAsync(catalogId, cancellationToken); + return await Task.FromResult(catalogContainer.Metadata); + }, cancellationToken); - if (catalogContainer is not null) - { - if (ensureReadable && !AuthUtilities.IsCatalogReadable(catalogContainer.Id, catalogContainer.Metadata, catalogContainer.Owner, User)) - return StatusCode(StatusCodes.Status403Forbidden, $"The current user is not permitted to read the catalog {catalogId}."); + return response; + } - if (ensureWritable && !AuthUtilities.IsCatalogWritable(catalogContainer.Id, catalogContainer.Metadata, User)) - return StatusCode(StatusCodes.Status403Forbidden, $"The current user is not permitted to modify the catalog {catalogId}."); + /// + /// Puts the catalog metadata. + /// + /// The catalog identifier. + /// The catalog metadata to set. + /// A token to cancel the current operation. + [HttpPut("{catalogId}/metadata")] + public Task + SetMetadataAsync( + string catalogId, + [FromBody] CatalogMetadata metadata, + CancellationToken cancellationToken) + { + catalogId = WebUtility.UrlDecode(catalogId); + + var response = ProtectCatalogAsync(catalogId, ensureReadable: true, ensureWritable: false, async catalogContainer => + { + var canEdit = AuthUtilities.IsCatalogWritable(catalogId, catalogContainer.Metadata, User); - return await action.Invoke(catalogContainer); + if (!canEdit) + return StatusCode(StatusCodes.Status403Forbidden, $"The current user is not permitted to modify the catalog {catalogId}."); + + await catalogContainer.UpdateMetadataAsync(metadata); + + return new object(); + + }, cancellationToken); + + return response; + } + + private async Task> ProtectCatalogAsync( + string catalogId, + bool ensureReadable, + bool ensureWritable, + Func>> action, + CancellationToken cancellationToken) + { + var root = _appState.CatalogState.Root; + + var catalogContainer = catalogId == CatalogContainer.RootCatalogId + ? root + : await root.TryFindCatalogContainerAsync(catalogId, cancellationToken); + + if (catalogContainer is not null) + { + if (ensureReadable && !AuthUtilities.IsCatalogReadable( + catalogContainer.Id, catalogContainer.Metadata, catalogContainer.Owner, User)) + { + return StatusCode( + StatusCodes.Status403Forbidden, + $"The current user is not permitted to read the catalog {catalogId}."); } - else + + if (ensureWritable && !AuthUtilities.IsCatalogWritable( + catalogContainer.Id, catalogContainer.Metadata, User)) { - return NotFound(catalogId); + return StatusCode( + StatusCodes.Status403Forbidden, + $"The current user is not permitted to modify the catalog {catalogId}."); } + + return await action.Invoke(catalogContainer); } + else + { + return NotFound(catalogId); + } + } + + private async Task ProtectCatalogNonGenericAsync( + string catalogId, + bool ensureReadable, + bool ensureWritable, + Func> action, + CancellationToken cancellationToken) + { + var root = _appState.CatalogState.Root; + var catalogContainer = await root.TryFindCatalogContainerAsync(catalogId, cancellationToken); + + if (catalogContainer is not null) + { + if (ensureReadable && !AuthUtilities.IsCatalogReadable(catalogContainer.Id, catalogContainer.Metadata, catalogContainer.Owner, User)) + return StatusCode(StatusCodes.Status403Forbidden, $"The current user is not permitted to read the catalog {catalogId}."); + + if (ensureWritable && !AuthUtilities.IsCatalogWritable(catalogContainer.Id, catalogContainer.Metadata, User)) + return StatusCode(StatusCodes.Status403Forbidden, $"The current user is not permitted to modify the catalog {catalogId}."); - #endregion + return await action.Invoke(catalogContainer); + } + else + { + return NotFound(catalogId); + } } + + #endregion } diff --git a/src/Nexus/API/DataController.cs b/src/Nexus/API/DataController.cs index b9dd9447..7e4a58c6 100644 --- a/src/Nexus/API/DataController.cs +++ b/src/Nexus/API/DataController.cs @@ -5,74 +5,73 @@ using System.ComponentModel.DataAnnotations; using System.Net; -namespace Nexus.Controllers +namespace Nexus.Controllers; + +/// +/// Provides access to data. +/// +[Authorize] +[ApiController] +[ApiVersion("1.0")] +[Route("api/v{version:apiVersion}/[controller]")] +internal class DataController : ControllerBase { - /// - /// Provides access to data. - /// - [Authorize] - [ApiController] - [ApiVersion("1.0")] - [Route("api/v{version:apiVersion}/[controller]")] - internal class DataController : ControllerBase - { - // GET /api/data + // GET /api/data - #region Fields + #region Fields - private readonly IDataService _dataService; + private readonly IDataService _dataService; - #endregion + #endregion - #region Constructors + #region Constructors - public DataController( - IDataService dataService) - { - _dataService = dataService; - } + public DataController( + IDataService dataService) + { + _dataService = dataService; + } - #endregion + #endregion - /// - /// Gets the requested data. - /// - /// The path to the resource data to stream. - /// Start date/time. - /// End date/time. - /// A cancellation token. - /// + /// + /// Gets the requested data. + /// + /// The path to the resource data to stream. + /// Start date/time. + /// End date/time. + /// A cancellation token. + /// - [HttpGet] - public async Task GetStreamAsync( - [BindRequired] string resourcePath, - [BindRequired] DateTime begin, - [BindRequired] DateTime end, - CancellationToken cancellationToken) - { - resourcePath = WebUtility.UrlDecode(resourcePath); - begin = begin.ToUniversalTime(); - end = end.ToUniversalTime(); + [HttpGet] + public async Task GetStreamAsync( + [BindRequired] string resourcePath, + [BindRequired] DateTime begin, + [BindRequired] DateTime end, + CancellationToken cancellationToken) + { + resourcePath = WebUtility.UrlDecode(resourcePath); + begin = begin.ToUniversalTime(); + end = end.ToUniversalTime(); - try - { - var stream = await _dataService.ReadAsStreamAsync(resourcePath, begin, end, cancellationToken); + try + { + var stream = await _dataService.ReadAsStreamAsync(resourcePath, begin, end, cancellationToken); - Response.Headers.ContentLength = stream.Length; - return File(stream, "application/octet-stream", "data.bin"); - } - catch (ValidationException ex) - { - return UnprocessableEntity(ex.Message); - } - catch (Exception ex) when (ex.Message.StartsWith("Could not find resource path")) - { - return NotFound(ex.Message); - } - catch (Exception ex) when (ex.Message.StartsWith("The current user is not permitted to access the catalog")) - { - return StatusCode(StatusCodes.Status403Forbidden, ex.Message); - } + Response.Headers.ContentLength = stream.Length; + return File(stream, "application/octet-stream", "data.bin"); + } + catch (ValidationException ex) + { + return UnprocessableEntity(ex.Message); + } + catch (Exception ex) when (ex.Message.StartsWith("Could not find resource path")) + { + return NotFound(ex.Message); + } + catch (Exception ex) when (ex.Message.StartsWith("The current user is not permitted to access the catalog")) + { + return StatusCode(StatusCodes.Status403Forbidden, ex.Message); } } } diff --git a/src/Nexus/API/JobsController.cs b/src/Nexus/API/JobsController.cs index 02ba3c83..51cb327f 100644 --- a/src/Nexus/API/JobsController.cs +++ b/src/Nexus/API/JobsController.cs @@ -6,60 +6,85 @@ using Nexus.Utilities; using System.ComponentModel.DataAnnotations; -namespace Nexus.Controllers +namespace Nexus.Controllers; + +/// +/// Provides access to jobs. +/// +[Authorize] +[ApiController] +[ApiVersion("1.0")] +[Route("api/v{version:apiVersion}/[controller]")] +internal class JobsController : ControllerBase { + // GET /jobs + // DELETE /jobs{jobId} + // GET /jobs{jobId}/status + // POST /jobs/export + // POST /jobs/load-packages + // POST /jobs/clear-cache + + #region Fields + + private readonly AppStateManager _appStateManager; + private readonly ILogger _logger; + private readonly IServiceProvider _serviceProvider; + private readonly Serilog.IDiagnosticContext _diagnosticContext; + private readonly IJobService _jobService; + + #endregion + + #region Constructors + + public JobsController( + AppStateManager appStateManager, + IJobService jobService, + IServiceProvider serviceProvider, + Serilog.IDiagnosticContext diagnosticContext, + ILogger logger) + { + _appStateManager = appStateManager; + _jobService = jobService; + _serviceProvider = serviceProvider; + _diagnosticContext = diagnosticContext; + _logger = logger; + } + + #endregion + + #region Jobs Management + /// - /// Provides access to jobs. + /// Gets a list of jobs. /// - [Authorize] - [ApiController] - [ApiVersion("1.0")] - [Route("api/v{version:apiVersion}/[controller]")] - internal class JobsController : ControllerBase + /// + [HttpGet] + public ActionResult> GetJobs() { - // GET /jobs - // DELETE /jobs{jobId} - // GET /jobs{jobId}/status - // POST /jobs/export - // POST /jobs/load-packages - // POST /jobs/clear-cache - - #region Fields - - private readonly AppStateManager _appStateManager; - private readonly ILogger _logger; - private readonly IServiceProvider _serviceProvider; - private readonly Serilog.IDiagnosticContext _diagnosticContext; - private readonly IJobService _jobService; - - #endregion - - #region Constructors - - public JobsController( - AppStateManager appStateManager, - IJobService jobService, - IServiceProvider serviceProvider, - Serilog.IDiagnosticContext diagnosticContext, - ILogger logger) - { - _appStateManager = appStateManager; - _jobService = jobService; - _serviceProvider = serviceProvider; - _diagnosticContext = diagnosticContext; - _logger = logger; - } + var isAdmin = User.IsInRole(NexusRoles.ADMINISTRATOR); + var username = User.Identity?.Name; - #endregion + if (username is null) + throw new Exception("This should never happen."); - #region Jobs Management + var result = _jobService + .GetJobs() + .Select(jobControl => jobControl.Job) + .Where(job => job.Owner == username || isAdmin) + .ToList(); - /// - /// Gets a list of jobs. - /// - /// - [HttpGet] - public ActionResult> GetJobs() + return result; + } + + /// + /// Cancels the specified job. + /// + /// + /// + [HttpDelete("{jobId}")] + public ActionResult CancelJob(Guid jobId) + { + if (_jobService.TryGetJob(jobId, out var jobControl)) { var isAdmin = User.IsInRole(NexusRoles.ADMINISTRATOR); var username = User.Identity?.Name; @@ -67,286 +92,260 @@ public ActionResult> GetJobs() if (username is null) throw new Exception("This should never happen."); - var result = _jobService - .GetJobs() - .Select(jobControl => jobControl.Job) - .Where(job => job.Owner == username || isAdmin) - .ToList(); - - return result; - } - - /// - /// Cancels the specified job. - /// - /// - /// - [HttpDelete("{jobId}")] - public ActionResult CancelJob(Guid jobId) - { - if (_jobService.TryGetJob(jobId, out var jobControl)) + if (jobControl.Job.Owner == username || isAdmin) { - var isAdmin = User.IsInRole(NexusRoles.ADMINISTRATOR); - var username = User.Identity?.Name; - - if (username is null) - throw new Exception("This should never happen."); - - if (jobControl.Job.Owner == username || isAdmin) - { - jobControl.CancellationTokenSource.Cancel(); - return Accepted(); - } - - else - { - return StatusCode(StatusCodes.Status403Forbidden, $"The current user is not permitted to cancel the job {jobControl.Job.Id}."); - } + jobControl.CancellationTokenSource.Cancel(); + return Accepted(); } else { - return NotFound(jobId); + return StatusCode(StatusCodes.Status403Forbidden, $"The current user is not permitted to cancel the job {jobControl.Job.Id}."); } } - /// - /// Gets the status of the specified job. - /// - /// - /// - [HttpGet("{jobId}/status")] - public async Task> GetJobStatusAsync(Guid jobId) + else { - if (_jobService.TryGetJob(jobId, out var jobControl)) - { - var isAdmin = User.IsInRole(NexusRoles.ADMINISTRATOR); - var username = User.Identity?.Name; + return NotFound(jobId); + } + } - if (username is null) - throw new Exception("This should never happen."); + /// + /// Gets the status of the specified job. + /// + /// + /// + [HttpGet("{jobId}/status")] + public async Task> GetJobStatusAsync(Guid jobId) + { + if (_jobService.TryGetJob(jobId, out var jobControl)) + { + var isAdmin = User.IsInRole(NexusRoles.ADMINISTRATOR); + var username = User.Identity?.Name; - if (jobControl.Job.Owner == username || isAdmin) - { - var status = new JobStatus( - Start: jobControl.Start, - Progress: jobControl.Progress, - Status: jobControl.Task.Status, - ExceptionMessage: jobControl.Task.Exception is not null - ? jobControl.Task.Exception.Message - : default, - Result: jobControl.Task.Status == TaskStatus.RanToCompletion && (await jobControl.Task) is not null - ? await jobControl.Task - : default); - - return status; - } - else - { - return StatusCode(StatusCodes.Status403Forbidden, $"The current user is not permitted to access the status of job {jobControl.Job.Id}."); - } + if (username is null) + throw new Exception("This should never happen."); + + if (jobControl.Job.Owner == username || isAdmin) + { + var status = new JobStatus( + Start: jobControl.Start, + Progress: jobControl.Progress, + Status: jobControl.Task.Status, + ExceptionMessage: jobControl.Task.Exception is not null + ? jobControl.Task.Exception.Message + : default, + Result: jobControl.Task.Status == TaskStatus.RanToCompletion && (await jobControl.Task) is not null + ? await jobControl.Task + : default); + + return status; } else { - return NotFound(jobId); + return StatusCode(StatusCodes.Status403Forbidden, $"The current user is not permitted to access the status of job {jobControl.Job.Id}."); } } + else + { + return NotFound(jobId); + } + } - #endregion + #endregion - #region Jobs + #region Jobs - /// - /// Creates a new export job. - /// - /// Export parameters. - /// The token to cancel the current operation. - /// - [HttpPost("export")] - public async Task> ExportAsync( - ExportParameters parameters, - CancellationToken cancellationToken) - { - _diagnosticContext.Set("Body", JsonSerializerHelper.SerializeIndented(parameters)); + /// + /// Creates a new export job. + /// + /// Export parameters. + /// The token to cancel the current operation. + /// + [HttpPost("export")] + public async Task> ExportAsync( + ExportParameters parameters, + CancellationToken cancellationToken) + { + _diagnosticContext.Set("Body", JsonSerializerHelper.SerializeIndented(parameters)); - parameters = parameters with - { - Begin = parameters.Begin.ToUniversalTime(), - End = parameters.End.ToUniversalTime() - }; + parameters = parameters with + { + Begin = parameters.Begin.ToUniversalTime(), + End = parameters.End.ToUniversalTime() + }; - var root = _appStateManager.AppState.CatalogState.Root; + var root = _appStateManager.AppState.CatalogState.Root; - // check that there is anything to export - if (!parameters.ResourcePaths.Any()) - return BadRequest("The list of resource paths is empty."); + // check that there is anything to export + if (!parameters.ResourcePaths.Any()) + return BadRequest("The list of resource paths is empty."); - // translate resource paths to catalog item requests - CatalogItemRequest[] catalogItemRequests; + // translate resource paths to catalog item requests + CatalogItemRequest[] catalogItemRequests; - try + try + { + catalogItemRequests = await Task.WhenAll(parameters.ResourcePaths.Select(async resourcePath => { - catalogItemRequests = await Task.WhenAll(parameters.ResourcePaths.Select(async resourcePath => - { - var catalogItemRequest = await root.TryFindAsync(resourcePath, cancellationToken); + var catalogItemRequest = await root.TryFindAsync(resourcePath, cancellationToken); - if (catalogItemRequest is null) - throw new ValidationException($"Could not find resource path {resourcePath}."); + if (catalogItemRequest is null) + throw new ValidationException($"Could not find resource path {resourcePath}."); - return catalogItemRequest; - })); - } - catch (ValidationException ex) + return catalogItemRequest; + })); + } + catch (ValidationException ex) + { + return UnprocessableEntity(ex.Message); + } + + // authorize + try + { + foreach (var group in catalogItemRequests.GroupBy(current => current.Container.Id)) { - return UnprocessableEntity(ex.Message); + var catalogContainer = group.First().Container; + + if (!AuthUtilities.IsCatalogReadable(catalogContainer.Id, catalogContainer.Metadata, catalogContainer.Owner, User)) + throw new UnauthorizedAccessException($"The current user is not permitted to access catalog {catalogContainer.Id}."); } + } + catch (UnauthorizedAccessException ex) + { + return StatusCode(StatusCodes.Status403Forbidden, ex.Message); + } - // authorize - try + // + var username = User.Identity?.Name!; + var job = new Job(Guid.NewGuid(), "export", username, parameters); + var dataService = _serviceProvider.GetRequiredService(); + + try + { + var jobControl = _jobService.AddJob(job, dataService.WriteProgress, async (jobControl, cts) => { - foreach (var group in catalogItemRequests.GroupBy(current => current.Container.Id)) + try { - var catalogContainer = group.First().Container; - - if (!AuthUtilities.IsCatalogReadable(catalogContainer.Id, catalogContainer.Metadata, catalogContainer.Owner, User)) - throw new UnauthorizedAccessException($"The current user is not permitted to access catalog {catalogContainer.Id}."); + var result = await dataService.ExportAsync(job.Id, catalogItemRequests, dataService.ReadAsDoubleAsync, parameters, cts.Token); + return result; } - } - catch (UnauthorizedAccessException ex) - { - return StatusCode(StatusCodes.Status403Forbidden, ex.Message); - } + catch (Exception ex) + { + _logger.LogError(ex, "Unable to export the requested data."); + throw; + } + }); + + return Accepted(GetAcceptUrl(job.Id), job); + } + catch (ValidationException ex) + { + return UnprocessableEntity(ex.Message); + } + } - // - var username = User.Identity?.Name!; - var job = new Job(Guid.NewGuid(), "export", username, parameters); - var dataService = _serviceProvider.GetRequiredService(); + /// + /// Creates a new job which reloads all extensions and resets the resource catalog. + /// + [HttpPost("refresh-database")] + public ActionResult RefreshDatabase() + { + var username = User.Identity?.Name!; + var job = new Job(Guid.NewGuid(), "refresh-database", username, default); + var progress = new Progress(); + + var jobControl = _jobService.AddJob(job, progress, async (jobControl, cts) => + { try { - var jobControl = _jobService.AddJob(job, dataService.WriteProgress, async (jobControl, cts) => - { - try - { - var result = await dataService.ExportAsync(job.Id, catalogItemRequests, dataService.ReadAsDoubleAsync, parameters, cts.Token); - return result; - } - catch (Exception ex) - { - _logger.LogError(ex, "Unable to export the requested data."); - throw; - } - }); - - return Accepted(GetAcceptUrl(job.Id), job); + await _appStateManager.RefreshDatabaseAsync(progress, cts.Token); + return null; } - catch (ValidationException ex) + catch (Exception ex) { - return UnprocessableEntity(ex.Message); + _logger.LogError(ex, "Unable to reload extensions and reset the resource catalog."); + throw; } - } + }); - /// - /// Creates a new job which reloads all extensions and resets the resource catalog. - /// - [HttpPost("refresh-database")] - public ActionResult RefreshDatabase() - { - var username = User.Identity?.Name!; + var response = (ActionResult)Accepted(GetAcceptUrl(job.Id), job); + return response; + } + + /// + /// Clears the aggregation data cache for the specified period of time. + /// + /// The catalog identifier. + /// Start date/time. + /// End date/time. + /// A token to cancel the current operation. + [HttpPost("clear-cache")] + public async Task> ClearCacheAsync( + [BindRequired] string catalogId, + [BindRequired] DateTime begin, + [BindRequired] DateTime end, + CancellationToken cancellationToken) + { + var username = User.Identity?.Name!; + var job = new Job(Guid.NewGuid(), "clear-cache", username, default); - var job = new Job(Guid.NewGuid(), "refresh-database", username, default); + var response = await ProtectCatalogNonGenericAsync(catalogId, catalogContainer => + { var progress = new Progress(); + var cacheService = _serviceProvider.GetRequiredService(); var jobControl = _jobService.AddJob(job, progress, async (jobControl, cts) => { try { - await _appStateManager.RefreshDatabaseAsync(progress, cts.Token); + await cacheService.ClearAsync(catalogId, begin, end, progress, cts.Token); return null; } catch (Exception ex) { - _logger.LogError(ex, "Unable to reload extensions and reset the resource catalog."); + _logger.LogError(ex, "Unable to clear the cache."); throw; } }); - var response = (ActionResult)Accepted(GetAcceptUrl(job.Id), job); - return response; - } + return Task.FromResult(Accepted(GetAcceptUrl(job.Id), job)); + }, cancellationToken); - /// - /// Clears the aggregation data cache for the specified period of time. - /// - /// The catalog identifier. - /// Start date/time. - /// End date/time. - /// A token to cancel the current operation. - [HttpPost("clear-cache")] - public async Task> ClearCacheAsync( - [BindRequired] string catalogId, - [BindRequired] DateTime begin, - [BindRequired] DateTime end, - CancellationToken cancellationToken) - { - var username = User.Identity?.Name!; - var job = new Job(Guid.NewGuid(), "clear-cache", username, default); + return (ActionResult)response; + } - var response = await ProtectCatalogNonGenericAsync(catalogId, catalogContainer => - { - var progress = new Progress(); - var cacheService = _serviceProvider.GetRequiredService(); + #endregion - var jobControl = _jobService.AddJob(job, progress, async (jobControl, cts) => - { - try - { - await cacheService.ClearAsync(catalogId, begin, end, progress, cts.Token); - return null; - } - catch (Exception ex) - { - _logger.LogError(ex, "Unable to clear the cache."); - throw; - } - }); - - return Task.FromResult(Accepted(GetAcceptUrl(job.Id), job)); - }, cancellationToken); - - return (ActionResult)response; - } + #region Methods - #endregion + private string GetAcceptUrl(Guid jobId) + { + return $"{Request.Scheme}://{Request.Host}{Request.Path}/{jobId}/status"; + } - #region Methods + private async Task ProtectCatalogNonGenericAsync( + string catalogId, + Func> action, + CancellationToken cancellationToken) + { + var root = _appStateManager.AppState.CatalogState.Root; + var catalogContainer = await root.TryFindCatalogContainerAsync(catalogId, cancellationToken); - private string GetAcceptUrl(Guid jobId) + if (catalogContainer is not null) { - return $"{Request.Scheme}://{Request.Host}{Request.Path}/{jobId}/status"; - } + if (!AuthUtilities.IsCatalogWritable(catalogContainer.Id, catalogContainer.Metadata, User)) + return StatusCode(StatusCodes.Status403Forbidden, $"The current user is not permitted to modify the catalog {catalogId}."); - private async Task ProtectCatalogNonGenericAsync( - string catalogId, - Func> action, - CancellationToken cancellationToken) + return await action.Invoke(catalogContainer); + } + else { - var root = _appStateManager.AppState.CatalogState.Root; - var catalogContainer = await root.TryFindCatalogContainerAsync(catalogId, cancellationToken); - - if (catalogContainer is not null) - { - if (!AuthUtilities.IsCatalogWritable(catalogContainer.Id, catalogContainer.Metadata, User)) - return StatusCode(StatusCodes.Status403Forbidden, $"The current user is not permitted to modify the catalog {catalogId}."); - - return await action.Invoke(catalogContainer); - } - else - { - return NotFound(catalogId); - } + return NotFound(catalogId); } - - #endregion } + + #endregion } diff --git a/src/Nexus/API/PackageReferencesController.cs b/src/Nexus/API/PackageReferencesController.cs index f024f2f8..79f9846d 100644 --- a/src/Nexus/API/PackageReferencesController.cs +++ b/src/Nexus/API/PackageReferencesController.cs @@ -3,109 +3,108 @@ using Nexus.Core; using Nexus.Services; -namespace Nexus.Controllers +namespace Nexus.Controllers; + +/// +/// Provides access to package references. +/// +[Authorize(Policy = NexusPolicies.RequireAdmin)] +[ApiController] +[ApiVersion("1.0")] +[Route("api/v{version:apiVersion}/[controller]")] +internal class PackageReferencesController : ControllerBase { + // GET /api/packagereferences + // POST /api/packagereferences + // DELETE /api/packagereferences/{packageReferenceId} + // GET /api/packagereferences/{packageReferenceId}/versions + + #region Fields + + private readonly AppState _appState; + private readonly AppStateManager _appStateManager; + private readonly IExtensionHive _extensionHive; + + #endregion + + #region Constructors + + public PackageReferencesController( + AppState appState, + AppStateManager appStateManager, + IExtensionHive extensionHive) + { + _appState = appState; + _appStateManager = appStateManager; + _extensionHive = extensionHive; + } + + #endregion + + #region Methods + /// - /// Provides access to package references. + /// Gets the list of package references. /// - [Authorize(Policy = NexusPolicies.RequireAdmin)] - [ApiController] - [ApiVersion("1.0")] - [Route("api/v{version:apiVersion}/[controller]")] - internal class PackageReferencesController : ControllerBase + /// + [HttpGet] + public IDictionary Get() { - // GET /api/packagereferences - // POST /api/packagereferences - // DELETE /api/packagereferences/{packageReferenceId} - // GET /api/packagereferences/{packageReferenceId}/versions - - #region Fields - - private readonly AppState _appState; - private readonly AppStateManager _appStateManager; - private readonly IExtensionHive _extensionHive; - - #endregion - - #region Constructors - - public PackageReferencesController( - AppState appState, - AppStateManager appStateManager, - IExtensionHive extensionHive) - { - _appState = appState; - _appStateManager = appStateManager; - _extensionHive = extensionHive; - } - - #endregion - - #region Methods - - /// - /// Gets the list of package references. - /// - /// - [HttpGet] - public IDictionary Get() - { - return _appState.Project.PackageReferences - .ToDictionary( - entry => entry.Key, - entry => new PackageReference(entry.Value.Provider, entry.Value.Configuration)); - } - - /// - /// Creates a package reference. - /// - /// The package reference to create. - [HttpPost] - public async Task CreateAsync( - [FromBody] PackageReference packageReference) - { - var internalPackageReference = new InternalPackageReference( - Id: Guid.NewGuid(), - Provider: packageReference.Provider, - Configuration: packageReference.Configuration); - - await _appStateManager.PutPackageReferenceAsync(internalPackageReference); - - return internalPackageReference.Id; - } - - /// - /// Deletes a package reference. - /// - /// The ID of the package reference. - [HttpDelete("{packageReferenceId}")] - public Task DeleteAsync( - Guid packageReferenceId) - { - return _appStateManager.DeletePackageReferenceAsync(packageReferenceId); - } - - /// - /// Gets package versions. - /// - /// The ID of the package reference. - /// A token to cancel the current operation. - [HttpGet("{packageReferenceId}/versions")] - public async Task> GetVersionsAsync( - Guid packageReferenceId, - CancellationToken cancellationToken) - { - var project = _appState.Project; - - if (!project.PackageReferences.TryGetValue(packageReferenceId, out var packageReference)) - return NotFound($"Unable to find package reference with ID {packageReferenceId}."); - - var result = await _extensionHive - .GetVersionsAsync(packageReference, cancellationToken); - - return result; - } - - #endregion + return _appState.Project.PackageReferences + .ToDictionary( + entry => entry.Key, + entry => new PackageReference(entry.Value.Provider, entry.Value.Configuration)); } + + /// + /// Creates a package reference. + /// + /// The package reference to create. + [HttpPost] + public async Task CreateAsync( + [FromBody] PackageReference packageReference) + { + var internalPackageReference = new InternalPackageReference( + Id: Guid.NewGuid(), + Provider: packageReference.Provider, + Configuration: packageReference.Configuration); + + await _appStateManager.PutPackageReferenceAsync(internalPackageReference); + + return internalPackageReference.Id; + } + + /// + /// Deletes a package reference. + /// + /// The ID of the package reference. + [HttpDelete("{packageReferenceId}")] + public Task DeleteAsync( + Guid packageReferenceId) + { + return _appStateManager.DeletePackageReferenceAsync(packageReferenceId); + } + + /// + /// Gets package versions. + /// + /// The ID of the package reference. + /// A token to cancel the current operation. + [HttpGet("{packageReferenceId}/versions")] + public async Task> GetVersionsAsync( + Guid packageReferenceId, + CancellationToken cancellationToken) + { + var project = _appState.Project; + + if (!project.PackageReferences.TryGetValue(packageReferenceId, out var packageReference)) + return NotFound($"Unable to find package reference with ID {packageReferenceId}."); + + var result = await _extensionHive + .GetVersionsAsync(packageReference, cancellationToken); + + return result; + } + + #endregion } \ No newline at end of file diff --git a/src/Nexus/API/SourcesController.cs b/src/Nexus/API/SourcesController.cs index c4595b81..72c5df84 100644 --- a/src/Nexus/API/SourcesController.cs +++ b/src/Nexus/API/SourcesController.cs @@ -8,181 +8,180 @@ using System.Security.Claims; using static OpenIddict.Abstractions.OpenIddictConstants; -namespace Nexus.Controllers +namespace Nexus.Controllers; + +/// +/// Provides access to extensions. +/// +[Authorize] +[ApiController] +[ApiVersion("1.0")] +[Route("api/v{version:apiVersion}/[controller]")] +internal class SourcesController : ControllerBase { - /// - /// Provides access to extensions. - /// - [Authorize] - [ApiController] - [ApiVersion("1.0")] - [Route("api/v{version:apiVersion}/[controller]")] - internal class SourcesController : ControllerBase - { - // GET /api/sources/descriptions - // GET /api/sources/registrations - // POST /api/sources/registrations - // DELETE /api/sources/registrations/{registrationId} + // GET /api/sources/descriptions + // GET /api/sources/registrations + // POST /api/sources/registrations + // DELETE /api/sources/registrations/{registrationId} - #region Fields + #region Fields - private readonly AppState _appState; - private readonly AppStateManager _appStateManager; - private readonly IExtensionHive _extensionHive; + private readonly AppState _appState; + private readonly AppStateManager _appStateManager; + private readonly IExtensionHive _extensionHive; - #endregion + #endregion - #region Constructors + #region Constructors - public SourcesController( - AppState appState, - AppStateManager appStateManager, - IExtensionHive extensionHive) - { - _appState = appState; - _appStateManager = appStateManager; - _extensionHive = extensionHive; - } + public SourcesController( + AppState appState, + AppStateManager appStateManager, + IExtensionHive extensionHive) + { + _appState = appState; + _appStateManager = appStateManager; + _extensionHive = extensionHive; + } - #endregion + #endregion - /// - /// Gets the list of source descriptions. - /// - [HttpGet("descriptions")] - public List GetDescriptions() - { - var result = GetExtensionDescriptions(_extensionHive.GetExtensions()); - return result; - } + /// + /// Gets the list of source descriptions. + /// + [HttpGet("descriptions")] + public List GetDescriptions() + { + var result = GetExtensionDescriptions(_extensionHive.GetExtensions()); + return result; + } - /// - /// Gets the list of data source registrations. - /// - /// The optional user identifier. If not specified, the current user will be used. - /// - [HttpGet("registrations")] - public ActionResult> GetRegistrations( - [FromQuery] string? userId = default) + /// + /// Gets the list of data source registrations. + /// + /// The optional user identifier. If not specified, the current user will be used. + /// + [HttpGet("registrations")] + public ActionResult> GetRegistrations( + [FromQuery] string? userId = default) + { + if (TryAuthenticate(userId, out var actualUserId, out var response)) { - if (TryAuthenticate(userId, out var actualUserId, out var response)) - { - if (_appState.Project.UserConfigurations.TryGetValue(actualUserId, out var userConfiguration)) - return Ok(userConfiguration.DataSourceRegistrations - .ToDictionary( - entry => entry.Key, - entry => new DataSourceRegistration( - entry.Value.Type, - entry.Value.ResourceLocator, - entry.Value.Configuration, - entry.Value.InfoUrl, - entry.Value.ReleasePattern, - entry.Value.VisibilityPattern))); - - else - return Ok(new Dictionary()); - } + if (_appState.Project.UserConfigurations.TryGetValue(actualUserId, out var userConfiguration)) + return Ok(userConfiguration.DataSourceRegistrations + .ToDictionary( + entry => entry.Key, + entry => new DataSourceRegistration( + entry.Value.Type, + entry.Value.ResourceLocator, + entry.Value.Configuration, + entry.Value.InfoUrl, + entry.Value.ReleasePattern, + entry.Value.VisibilityPattern))); else - { - return response; - } + return Ok(new Dictionary()); } - /// - /// Creates a data source registration. - /// - /// The registration to create. - /// The optional user identifier. If not specified, the current user will be used. - [HttpPost("registrations")] - public async Task> CreateRegistrationAsync( - DataSourceRegistration registration, - [FromQuery] string? userId = default) + else { - if (TryAuthenticate(userId, out var actualUserId, out var response)) - { - var internalRegistration = new InternalDataSourceRegistration( - Id: Guid.NewGuid(), - registration.Type, - registration.ResourceLocator, - registration.Configuration, - registration.InfoUrl, - registration.ReleasePattern, - registration.VisibilityPattern); - - await _appStateManager.PutDataSourceRegistrationAsync(actualUserId, internalRegistration); - return Ok(internalRegistration.Id); - } + return response; + } + } - else - { - return response; - } + /// + /// Creates a data source registration. + /// + /// The registration to create. + /// The optional user identifier. If not specified, the current user will be used. + [HttpPost("registrations")] + public async Task> CreateRegistrationAsync( + DataSourceRegistration registration, + [FromQuery] string? userId = default) + { + if (TryAuthenticate(userId, out var actualUserId, out var response)) + { + var internalRegistration = new InternalDataSourceRegistration( + Id: Guid.NewGuid(), + registration.Type, + registration.ResourceLocator, + registration.Configuration, + registration.InfoUrl, + registration.ReleasePattern, + registration.VisibilityPattern); + + await _appStateManager.PutDataSourceRegistrationAsync(actualUserId, internalRegistration); + return Ok(internalRegistration.Id); } - /// - /// Deletes a data source registration. - /// - /// The identifier of the registration. - /// The optional user identifier. If not specified, the current user will be used. - [HttpDelete("registrations/{registrationId}")] - public async Task DeleteRegistrationAsync( - Guid registrationId, - [FromQuery] string? userId = default) + else { - if (TryAuthenticate(userId, out var actualUserId, out var response)) - { - await _appStateManager.DeleteDataSourceRegistrationAsync(actualUserId, registrationId); - return Ok(); - } + return response; + } + } - else - { - return response; - } + /// + /// Deletes a data source registration. + /// + /// The identifier of the registration. + /// The optional user identifier. If not specified, the current user will be used. + [HttpDelete("registrations/{registrationId}")] + public async Task DeleteRegistrationAsync( + Guid registrationId, + [FromQuery] string? userId = default) + { + if (TryAuthenticate(userId, out var actualUserId, out var response)) + { + await _appStateManager.DeleteDataSourceRegistrationAsync(actualUserId, registrationId); + return Ok(); } - private static List GetExtensionDescriptions( - IEnumerable extensions) + else { - return extensions.Select(type => - { - var version = type.Assembly - .GetCustomAttribute()! - .InformationalVersion; - - var attribute = type - .GetCustomAttribute(inherit: false); - - if (attribute is null) - return new ExtensionDescription(type.FullName!, version, default, default, default, default); - - else - return new ExtensionDescription(type.FullName!, version, attribute.Description, attribute.ProjectUrl, attribute.RepositoryUrl, default); - }) - .ToList(); + return response; } + } - // TODO: code duplication (UsersController) - private bool TryAuthenticate( - string? requestedId, - out string userId, - [NotNullWhen(returnValue: false)] out ActionResult? response) + private static List GetExtensionDescriptions( + IEnumerable extensions) + { + return extensions.Select(type => { - var isAdmin = User.IsInRole(NexusRoles.ADMINISTRATOR); - var currentId = User.FindFirstValue(Claims.Subject) ?? throw new Exception("The sub claim is null."); + var version = type.Assembly + .GetCustomAttribute()! + .InformationalVersion; + + var attribute = type + .GetCustomAttribute(inherit: false); - if (isAdmin || requestedId is null || requestedId == currentId) - response = null; + if (attribute is null) + return new ExtensionDescription(type.FullName!, version, default, default, default, default); else - response = StatusCode(StatusCodes.Status403Forbidden, $"The current user is not permitted to get source registrations of user {requestedId}."); + return new ExtensionDescription(type.FullName!, version, attribute.Description, attribute.ProjectUrl, attribute.RepositoryUrl, default); + }) + .ToList(); + } - userId = requestedId is null - ? currentId - : requestedId; + // TODO: code duplication (UsersController) + private bool TryAuthenticate( + string? requestedId, + out string userId, + [NotNullWhen(returnValue: false)] out ActionResult? response) + { + var isAdmin = User.IsInRole(NexusRoles.ADMINISTRATOR); + var currentId = User.FindFirstValue(Claims.Subject) ?? throw new Exception("The sub claim is null."); - return response is null; - } + if (isAdmin || requestedId is null || requestedId == currentId) + response = null; + + else + response = StatusCode(StatusCodes.Status403Forbidden, $"The current user is not permitted to get source registrations of user {requestedId}."); + + userId = requestedId is null + ? currentId + : requestedId; + + return response is null; } } diff --git a/src/Nexus/API/SystemController.cs b/src/Nexus/API/SystemController.cs index ff93dd52..b80a7b1a 100644 --- a/src/Nexus/API/SystemController.cs +++ b/src/Nexus/API/SystemController.cs @@ -5,86 +5,85 @@ using Nexus.Services; using System.Text.Json; -namespace Nexus.Controllers -{ - /// - /// Provides access to the system. - /// - [Authorize] - [ApiController] - [ApiVersion("1.0")] - [Route("api/v{version:apiVersion}/[controller]")] - internal class SystemController : ControllerBase - { - // [authenticated] - // GET /api/system/configuration - // GET /api/system/file-type - // GET /api/system/help-link +namespace Nexus.Controllers; - // [privileged] - // PUT /api/system/configuration +/// +/// Provides access to the system. +/// +[Authorize] +[ApiController] +[ApiVersion("1.0")] +[Route("api/v{version:apiVersion}/[controller]")] +internal class SystemController : ControllerBase +{ + // [authenticated] + // GET /api/system/configuration + // GET /api/system/file-type + // GET /api/system/help-link - #region Fields + // [privileged] + // PUT /api/system/configuration - private readonly AppState _appState; - private readonly AppStateManager _appStateManager; - private readonly GeneralOptions _generalOptions; + #region Fields - #endregion + private readonly AppState _appState; + private readonly AppStateManager _appStateManager; + private readonly GeneralOptions _generalOptions; - #region Constructors + #endregion - public SystemController( - AppState appState, - AppStateManager appStateManager, - IOptions generalOptions) - { - _generalOptions = generalOptions.Value; - _appState = appState; - _appStateManager = appStateManager; - } + #region Constructors - #endregion + public SystemController( + AppState appState, + AppStateManager appStateManager, + IOptions generalOptions) + { + _generalOptions = generalOptions.Value; + _appState = appState; + _appStateManager = appStateManager; + } - #region Methods + #endregion - /// - /// Gets the default file type. - /// - [HttpGet("file-type")] - public string? GetDefaultFileType() - { - return _generalOptions.DefaultFileType; - } + #region Methods - /// - /// Gets the configured help link. - /// - [HttpGet("help-link")] - public string? GetHelpLink() - { - return _generalOptions.HelpLink; - } + /// + /// Gets the default file type. + /// + [HttpGet("file-type")] + public string? GetDefaultFileType() + { + return _generalOptions.DefaultFileType; + } - /// - /// Gets the system configuration. - /// - [HttpGet("configuration")] - public IReadOnlyDictionary? GetConfiguration() - { - return _appState.Project.SystemConfiguration; - } + /// + /// Gets the configured help link. + /// + [HttpGet("help-link")] + public string? GetHelpLink() + { + return _generalOptions.HelpLink; + } - /// - /// Sets the system configuration. - /// - [HttpPut("configuration")] - [Authorize(Policy = NexusPolicies.RequireAdmin)] - public Task SetConfigurationAsync(IReadOnlyDictionary? configuration) - { - return _appStateManager.PutSystemConfigurationAsync(configuration); - } + /// + /// Gets the system configuration. + /// + [HttpGet("configuration")] + public IReadOnlyDictionary? GetConfiguration() + { + return _appState.Project.SystemConfiguration; + } - #endregion + /// + /// Sets the system configuration. + /// + [HttpPut("configuration")] + [Authorize(Policy = NexusPolicies.RequireAdmin)] + public Task SetConfigurationAsync(IReadOnlyDictionary? configuration) + { + return _appStateManager.PutSystemConfigurationAsync(configuration); } + + #endregion } diff --git a/src/Nexus/API/UsersController.cs b/src/Nexus/API/UsersController.cs index f3d34ed5..16a5c513 100644 --- a/src/Nexus/API/UsersController.cs +++ b/src/Nexus/API/UsersController.cs @@ -13,426 +13,425 @@ using System.Security.Claims; using static OpenIddict.Abstractions.OpenIddictConstants; -namespace Nexus.Controllers +namespace Nexus.Controllers; + +/// +/// Provides access to users. +/// +[Authorize] +[ApiController] +[ApiVersion("1.0")] +[Route("api/v{version:apiVersion}/[controller]")] +internal class UsersController : ControllerBase { - /// - /// Provides access to users. - /// - [Authorize] - [ApiController] - [ApiVersion("1.0")] - [Route("api/v{version:apiVersion}/[controller]")] - internal class UsersController : ControllerBase - { - // [anonymous] - // GET /api/users/authentication-schemes - // GET /api/users/authenticate - // GET /api/users/signout - // POST /api/users/tokens/delete + // [anonymous] + // GET /api/users/authentication-schemes + // GET /api/users/authenticate + // GET /api/users/signout + // POST /api/users/tokens/delete - // [authenticated] - // GET /api/users/me - // GET /api/users/accept-license?catalogId=X - // POST /api/users/tokens/create - // DELETE /api/users/tokens/{tokenId} + // [authenticated] + // GET /api/users/me + // GET /api/users/accept-license?catalogId=X + // POST /api/users/tokens/create + // DELETE /api/users/tokens/{tokenId} - // [privileged] - // GET /api/users - // POST /api/users - // DELETE /api/users/{userId} + // [privileged] + // GET /api/users + // POST /api/users + // DELETE /api/users/{userId} - // GET /api/users/{userId}/claims - // POST /api/users/{userId}/claims - // DELETE /api/users/claims/{claimId} + // GET /api/users/{userId}/claims + // POST /api/users/{userId}/claims + // DELETE /api/users/claims/{claimId} - // GET /api/users/{userId}/tokens + // GET /api/users/{userId}/tokens - #region Fields + #region Fields - private readonly IDBService _dbService; - private readonly ITokenService _tokenService; - private readonly SecurityOptions _securityOptions; - private readonly ILogger _logger; + private readonly IDBService _dbService; + private readonly ITokenService _tokenService; + private readonly SecurityOptions _securityOptions; + private readonly ILogger _logger; - #endregion + #endregion - #region Constructors + #region Constructors - public UsersController( - IDBService dBService, - ITokenService tokenService, - IOptions securityOptions, - ILogger logger) - { - _dbService = dBService; - _tokenService = tokenService; - _securityOptions = securityOptions.Value; - _logger = logger; - } + public UsersController( + IDBService dBService, + ITokenService tokenService, + IOptions securityOptions, + ILogger logger) + { + _dbService = dBService; + _tokenService = tokenService; + _securityOptions = securityOptions.Value; + _logger = logger; + } - #endregion + #endregion - #region Anonymous + #region Anonymous - /// - /// Returns a list of available authentication schemes. - /// - [AllowAnonymous] - [HttpGet("authentication-schemes")] - public List GetAuthenticationSchemes() - { - var providers = _securityOptions.OidcProviders.Any() - ? _securityOptions.OidcProviders - : new List() { NexusAuthExtensions.DefaultProvider }; + /// + /// Returns a list of available authentication schemes. + /// + [AllowAnonymous] + [HttpGet("authentication-schemes")] + public List GetAuthenticationSchemes() + { + var providers = _securityOptions.OidcProviders.Any() + ? _securityOptions.OidcProviders + : new List() { NexusAuthExtensions.DefaultProvider }; - return providers - .Select(provider => new AuthenticationSchemeDescription(provider.Scheme, provider.DisplayName)) - .ToList(); - } + return providers + .Select(provider => new AuthenticationSchemeDescription(provider.Scheme, provider.DisplayName)) + .ToList(); + } - /// - /// Authenticates the user. - /// - /// The authentication scheme to challenge. - /// The URL to return after successful authentication. - [AllowAnonymous] - [HttpPost("authenticate")] - public ChallengeResult Authenticate( - [BindRequired] string scheme, - [BindRequired] string returnUrl) + /// + /// Authenticates the user. + /// + /// The authentication scheme to challenge. + /// The URL to return after successful authentication. + [AllowAnonymous] + [HttpPost("authenticate")] + public ChallengeResult Authenticate( + [BindRequired] string scheme, + [BindRequired] string returnUrl) + { + var properties = new AuthenticationProperties() { - var properties = new AuthenticationProperties() - { - RedirectUri = returnUrl - }; + RedirectUri = returnUrl + }; - return Challenge(properties, scheme); - } + return Challenge(properties, scheme); + } - /// - /// Logs out the user. - /// - /// The URL to return after logout. - [AllowAnonymous] - [HttpPost("signout")] - public async Task SignOutAsync( - [BindRequired] string returnUrl) - { - await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); + /// + /// Logs out the user. + /// + /// The URL to return after logout. + [AllowAnonymous] + [HttpPost("signout")] + public async Task SignOutAsync( + [BindRequired] string returnUrl) + { + await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); - var properties = new AuthenticationProperties() { RedirectUri = returnUrl }; - var scheme = User.Identity!.AuthenticationType!; + var properties = new AuthenticationProperties() { RedirectUri = returnUrl }; + var scheme = User.Identity!.AuthenticationType!; - await HttpContext.SignOutAsync(scheme, properties); - } + await HttpContext.SignOutAsync(scheme, properties); + } - /// - /// Deletes a personal access token. - /// - /// The personal access token to delete. - [AllowAnonymous] - [HttpDelete("tokens/delete")] - public async Task DeleteTokenByValueAsync( - [BindRequired] string value) - { - var (userId, secret) = AuthUtilities.TokenValueToComponents(value); - await _tokenService.DeleteAsync(userId, secret); + /// + /// Deletes a personal access token. + /// + /// The personal access token to delete. + [AllowAnonymous] + [HttpDelete("tokens/delete")] + public async Task DeleteTokenByValueAsync( + [BindRequired] string value) + { + var (userId, secret) = AuthUtilities.TokenValueToComponents(value); + await _tokenService.DeleteAsync(userId, secret); - return Ok(); - } + return Ok(); + } - #endregion + #endregion - #region Authenticated + #region Authenticated - /// - /// Gets the current user. - /// - [HttpGet("me")] - public async Task> GetMeAsync() + /// + /// Gets the current user. + /// + [HttpGet("me")] + public async Task> GetMeAsync() + { + var userId = User.FindFirst(Claims.Subject)!.Value; + var user = await _dbService.FindUserAsync(userId); + + if (user is null) + return NotFound($"Could not find user {userId}."); + + var isAdmin = user.Claims.Any( + claim => claim.Type == Claims.Role && + claim.Value == NexusRoles.ADMINISTRATOR); + + var tokenMap = await _tokenService.GetAllAsync(userId); + + var translatedTokenMap = tokenMap + .ToDictionary(entry => entry.Value.Id, entry => new PersonalAccessToken( + entry.Value.Description, + entry.Value.Expires, + entry.Value.Claims + )); + + return new MeResponse( + user.Id, + user, + isAdmin, + translatedTokenMap); + } + + /// + /// Creates a personal access token. + /// + /// The personal access token to create. + /// The optional user identifier. If not specified, the current user will be used. + [Authorize(AuthenticationSchemes = CookieAuthenticationDefaults.AuthenticationScheme)] + [HttpPost("tokens/create")] + public async Task> CreateTokenAsync( + PersonalAccessToken token, + [FromQuery] string? userId = default + ) + { + if (TryAuthenticate(userId, out var actualUserId, out var response)) { - var userId = User.FindFirst(Claims.Subject)!.Value; - var user = await _dbService.FindUserAsync(userId); + var user = await _dbService.FindUserAsync(actualUserId); if (user is null) return NotFound($"Could not find user {userId}."); - var isAdmin = user.Claims.Any( - claim => claim.Type == Claims.Role && - claim.Value == NexusRoles.ADMINISTRATOR); + var utcExpires = token.Expires.ToUniversalTime(); - var tokenMap = await _tokenService.GetAllAsync(userId); + var secret = await _tokenService + .CreateAsync( + actualUserId, + token.Description, + utcExpires, + token.Claims); - var translatedTokenMap = tokenMap - .ToDictionary(entry => entry.Value.Id, entry => new PersonalAccessToken( - entry.Value.Description, - entry.Value.Expires, - entry.Value.Claims - )); + var tokenValue = AuthUtilities.ComponentsToTokenValue(actualUserId, secret); - return new MeResponse( - user.Id, - user, - isAdmin, - translatedTokenMap); + return Ok(tokenValue); } - /// - /// Creates a personal access token. - /// - /// The personal access token to create. - /// The optional user identifier. If not specified, the current user will be used. - [Authorize(AuthenticationSchemes = CookieAuthenticationDefaults.AuthenticationScheme)] - [HttpPost("tokens/create")] - public async Task> CreateTokenAsync( - PersonalAccessToken token, - [FromQuery] string? userId = default - ) + else { - if (TryAuthenticate(userId, out var actualUserId, out var response)) - { - var user = await _dbService.FindUserAsync(actualUserId); - - if (user is null) - return NotFound($"Could not find user {userId}."); - - var utcExpires = token.Expires.ToUniversalTime(); - - var secret = await _tokenService - .CreateAsync( - actualUserId, - token.Description, - utcExpires, - token.Claims); - - var tokenValue = AuthUtilities.ComponentsToTokenValue(actualUserId, secret); - - return Ok(tokenValue); - } - - else - { - return response; - } + return response; } + } - /// - /// Deletes a personal access token. - /// - /// The identifier of the personal access token. - [HttpDelete("tokens/{tokenId}")] - public async Task DeleteTokenAsync( - Guid tokenId) - { - var userId = User.FindFirst(Claims.Subject)!.Value; - - await _tokenService.DeleteAsync(userId, tokenId); + /// + /// Deletes a personal access token. + /// + /// The identifier of the personal access token. + [HttpDelete("tokens/{tokenId}")] + public async Task DeleteTokenAsync( + Guid tokenId) + { + var userId = User.FindFirst(Claims.Subject)!.Value; - return Ok(); - } + await _tokenService.DeleteAsync(userId, tokenId); - /// - /// Accepts the license of the specified catalog. - /// - /// The catalog identifier. - [HttpGet("accept-license")] - public async Task AcceptLicenseAsync( - [BindRequired] string catalogId) - { - // TODO: Is this thread safe? Maybe yes, because of scoped EF context. - catalogId = WebUtility.UrlDecode(catalogId); + return Ok(); + } - var userId = User.FindFirst(Claims.Subject)!.Value; - var user = await _dbService.FindUserAsync(userId); + /// + /// Accepts the license of the specified catalog. + /// + /// The catalog identifier. + [HttpGet("accept-license")] + public async Task AcceptLicenseAsync( + [BindRequired] string catalogId) + { + // TODO: Is this thread safe? Maybe yes, because of scoped EF context. + catalogId = WebUtility.UrlDecode(catalogId); - if (user is null) - return NotFound($"Could not find user {userId}."); + var userId = User.FindFirst(Claims.Subject)!.Value; + var user = await _dbService.FindUserAsync(userId); - var claim = new NexusClaim(Guid.NewGuid(), NexusClaims.CAN_READ_CATALOG, catalogId); - user.Claims.Add(claim); + if (user is null) + return NotFound($"Could not find user {userId}."); - /* When the primary key is != Guid.Empty, EF thinks the entity - * already exists and tries to update it. Adding it explicitly - * will correctly mark the entity as "added". - */ - await _dbService.AddOrUpdateClaimAsync(claim); + var claim = new NexusClaim(Guid.NewGuid(), NexusClaims.CAN_READ_CATALOG, catalogId); + user.Claims.Add(claim); - foreach (var identity in User.Identities) - { - identity?.AddClaim(new Claim(NexusClaims.CAN_READ_CATALOG, catalogId)); - } + /* When the primary key is != Guid.Empty, EF thinks the entity + * already exists and tries to update it. Adding it explicitly + * will correctly mark the entity as "added". + */ + await _dbService.AddOrUpdateClaimAsync(claim); - await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, User); + foreach (var identity in User.Identities) + { + identity?.AddClaim(new Claim(NexusClaims.CAN_READ_CATALOG, catalogId)); + } - var redirectUrl = "/catalogs/" + WebUtility.UrlEncode(catalogId); + await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, User); - return Redirect(redirectUrl); - } + var redirectUrl = "/catalogs/" + WebUtility.UrlEncode(catalogId); - #endregion + return Redirect(redirectUrl); + } - #region Privileged + #endregion - /// - /// Gets a list of users. - /// - /// - [Authorize(Policy = NexusPolicies.RequireAdmin)] - [HttpGet] - public async Task>> GetUsersAsync() - { - var users = await _dbService.GetUsers() - .ToListAsync(); + #region Privileged - return users.ToDictionary(user => user.Id, user => user); - } + /// + /// Gets a list of users. + /// + /// + [Authorize(Policy = NexusPolicies.RequireAdmin)] + [HttpGet] + public async Task>> GetUsersAsync() + { + var users = await _dbService.GetUsers() + .ToListAsync(); - /// - /// Creates a user. - /// - /// The user to create. - [Authorize(Policy = NexusPolicies.RequireAdmin)] - [HttpPost] - public async Task CreateUserAsync( - [FromBody] NexusUser user) - { - user.Id = Guid.NewGuid().ToString(); - await _dbService.AddOrUpdateUserAsync(user); + return users.ToDictionary(user => user.Id, user => user); + } - return user.Id; - } + /// + /// Creates a user. + /// + /// The user to create. + [Authorize(Policy = NexusPolicies.RequireAdmin)] + [HttpPost] + public async Task CreateUserAsync( + [FromBody] NexusUser user) + { + user.Id = Guid.NewGuid().ToString(); + await _dbService.AddOrUpdateUserAsync(user); - /// - /// Deletes a user. - /// - /// The identifier of the user. - [Authorize(Policy = NexusPolicies.RequireAdmin)] - [HttpDelete("{userId}")] - public async Task DeleteUserAsync( - string userId) - { - await _dbService.DeleteUserAsync(userId); - return Ok(); - } + return user.Id; + } - /// - /// Gets all claims. - /// - /// The identifier of the user. - [Authorize(Policy = NexusPolicies.RequireAdmin)] - [HttpGet("{userId}/claims")] - public async Task>> GetClaimsAsync( - string userId) - { - var user = await _dbService.FindUserAsync(userId); + /// + /// Deletes a user. + /// + /// The identifier of the user. + [Authorize(Policy = NexusPolicies.RequireAdmin)] + [HttpDelete("{userId}")] + public async Task DeleteUserAsync( + string userId) + { + await _dbService.DeleteUserAsync(userId); + return Ok(); + } - if (user is null) - return NotFound($"Could not find user {userId}."); + /// + /// Gets all claims. + /// + /// The identifier of the user. + [Authorize(Policy = NexusPolicies.RequireAdmin)] + [HttpGet("{userId}/claims")] + public async Task>> GetClaimsAsync( + string userId) + { + var user = await _dbService.FindUserAsync(userId); - return Ok(user.Claims.ToDictionary(claim => claim.Id, claim => claim)); - } + if (user is null) + return NotFound($"Could not find user {userId}."); - /// - /// Creates a claim. - /// - /// The identifier of the user. - /// The claim to create. - [Authorize(Policy = NexusPolicies.RequireAdmin)] - [HttpPost("{userId}/claims")] - public async Task> CreateClaimAsync( - string userId, - [FromBody] NexusClaim claim) - { - // TODO: Is this thread safe? Maybe yes, because of scoped EF context. + return Ok(user.Claims.ToDictionary(claim => claim.Id, claim => claim)); + } - claim.Id = Guid.NewGuid(); + /// + /// Creates a claim. + /// + /// The identifier of the user. + /// The claim to create. + [Authorize(Policy = NexusPolicies.RequireAdmin)] + [HttpPost("{userId}/claims")] + public async Task> CreateClaimAsync( + string userId, + [FromBody] NexusClaim claim) + { + // TODO: Is this thread safe? Maybe yes, because of scoped EF context. - var user = await _dbService.FindUserAsync(userId); + claim.Id = Guid.NewGuid(); - if (user is null) - return NotFound($"Could not find user {userId}."); + var user = await _dbService.FindUserAsync(userId); - user.Claims.Add(claim); + if (user is null) + return NotFound($"Could not find user {userId}."); - /* When the primary key is != Guid.Empty, EF thinks the entity - * already exists and tries to update it. Adding it explicitly - * will correctly mark the entity as "added". - */ - await _dbService.AddOrUpdateClaimAsync(claim); + user.Claims.Add(claim); - return Ok(claim.Id); - } + /* When the primary key is != Guid.Empty, EF thinks the entity + * already exists and tries to update it. Adding it explicitly + * will correctly mark the entity as "added". + */ + await _dbService.AddOrUpdateClaimAsync(claim); - /// - /// Deletes a claim. - /// - /// The identifier of the claim. - [Authorize(Policy = NexusPolicies.RequireAdmin)] - [HttpDelete("claims/{claimId}")] - public async Task DeleteClaimAsync( - Guid claimId) - { - // TODO: Is this thread safe? Maybe yes, because of scoped EF context. + return Ok(claim.Id); + } - var claim = await _dbService.FindClaimAsync(claimId); + /// + /// Deletes a claim. + /// + /// The identifier of the claim. + [Authorize(Policy = NexusPolicies.RequireAdmin)] + [HttpDelete("claims/{claimId}")] + public async Task DeleteClaimAsync( + Guid claimId) + { + // TODO: Is this thread safe? Maybe yes, because of scoped EF context. - if (claim is null) - return NotFound($"Could not find claim {claimId}."); + var claim = await _dbService.FindClaimAsync(claimId); - claim.Owner.Claims.Remove(claim); + if (claim is null) + return NotFound($"Could not find claim {claimId}."); - await _dbService.SaveChangesAsync(); + claim.Owner.Claims.Remove(claim); - return Ok(); - } + await _dbService.SaveChangesAsync(); - /// - /// Gets all personal access tokens. - /// - /// The identifier of the user. - [Authorize(Policy = NexusPolicies.RequireAdmin)] - [HttpGet("{userId}/tokens")] - public async Task>> GetTokensAsync( - string userId) - { - var user = await _dbService.FindUserAsync(userId); + return Ok(); + } - if (user is null) - return NotFound($"Could not find user {userId}."); + /// + /// Gets all personal access tokens. + /// + /// The identifier of the user. + [Authorize(Policy = NexusPolicies.RequireAdmin)] + [HttpGet("{userId}/tokens")] + public async Task>> GetTokensAsync( + string userId) + { + var user = await _dbService.FindUserAsync(userId); - var tokenMap = await _tokenService.GetAllAsync(userId); + if (user is null) + return NotFound($"Could not find user {userId}."); - var translatedTokenMap = tokenMap - .ToDictionary(entry => entry.Value.Id, entry => new PersonalAccessToken( - entry.Value.Description, - entry.Value.Expires, - entry.Value.Claims - )); + var tokenMap = await _tokenService.GetAllAsync(userId); - return translatedTokenMap; - } + var translatedTokenMap = tokenMap + .ToDictionary(entry => entry.Value.Id, entry => new PersonalAccessToken( + entry.Value.Description, + entry.Value.Expires, + entry.Value.Claims + )); - private bool TryAuthenticate( - string? requestedId, - out string userId, - [NotNullWhen(returnValue: false)] out ActionResult? response) - { - var isAdmin = User.IsInRole(NexusRoles.ADMINISTRATOR); - var currentId = User.FindFirstValue(Claims.Subject) ?? throw new Exception("The sub claim is null."); + return translatedTokenMap; + } - if (isAdmin || requestedId is null || requestedId == currentId) - response = null; + private bool TryAuthenticate( + string? requestedId, + out string userId, + [NotNullWhen(returnValue: false)] out ActionResult? response) + { + var isAdmin = User.IsInRole(NexusRoles.ADMINISTRATOR); + var currentId = User.FindFirstValue(Claims.Subject) ?? throw new Exception("The sub claim is null."); - else - response = StatusCode(StatusCodes.Status403Forbidden, $"The current user is not permitted to perform the operation for user {requestedId}."); + if (isAdmin || requestedId is null || requestedId == currentId) + response = null; - userId = requestedId is null - ? currentId - : requestedId; + else + response = StatusCode(StatusCodes.Status403Forbidden, $"The current user is not permitted to perform the operation for user {requestedId}."); - return response is null; - } + userId = requestedId is null + ? currentId + : requestedId; - #endregion + return response is null; } + + #endregion } diff --git a/src/Nexus/API/WritersController.cs b/src/Nexus/API/WritersController.cs index 9578ee1c..db609ac1 100644 --- a/src/Nexus/API/WritersController.cs +++ b/src/Nexus/API/WritersController.cs @@ -2,46 +2,45 @@ using Microsoft.AspNetCore.Mvc; using Nexus.Core; -namespace Nexus.Controllers +namespace Nexus.Controllers; + +/// +/// Provides access to extensions. +/// +[Authorize] +[ApiController] +[ApiVersion("1.0")] +[Route("api/v{version:apiVersion}/[controller]")] +internal class WritersController : ControllerBase { - /// - /// Provides access to extensions. - /// - [Authorize] - [ApiController] - [ApiVersion("1.0")] - [Route("api/v{version:apiVersion}/[controller]")] - internal class WritersController : ControllerBase - { - // GET /api/writers/descriptions + // GET /api/writers/descriptions - #region Fields + #region Fields - private readonly AppState _appState; + private readonly AppState _appState; - #endregion + #endregion - #region Constructors + #region Constructors - public WritersController( - AppState appState) - { - _appState = appState; - } - - #endregion + public WritersController( + AppState appState) + { + _appState = appState; + } - #region Methods + #endregion - /// - /// Gets the list of writer descriptions. - /// - [HttpGet("descriptions")] - public List GetDescriptions() - { - return _appState.DataWriterDescriptions; - } + #region Methods - #endregion + /// + /// Gets the list of writer descriptions. + /// + [HttpGet("descriptions")] + public List GetDescriptions() + { + return _appState.DataWriterDescriptions; } + + #endregion } diff --git a/src/Nexus/Core/AppState.cs b/src/Nexus/Core/AppState.cs index 1f3a1349..6eecd72d 100644 --- a/src/Nexus/Core/AppState.cs +++ b/src/Nexus/Core/AppState.cs @@ -2,38 +2,37 @@ using System.Collections.Concurrent; using System.Reflection; -namespace Nexus.Core +namespace Nexus.Core; + +internal class AppState { - internal class AppState - { - #region Constructors + #region Constructors - public AppState() - { - var entryAssembly = Assembly.GetEntryAssembly()!; - var version = entryAssembly.GetName().Version!; + public AppState() + { + var entryAssembly = Assembly.GetEntryAssembly()!; + var version = entryAssembly.GetName().Version!; - Version = version.ToString(); - } + Version = version.ToString(); + } - #endregion + #endregion - #region Properties - General + #region Properties - General - public ConcurrentDictionary> ResourceCache { get; } - = new ConcurrentDictionary>(); + public ConcurrentDictionary> ResourceCache { get; } + = new ConcurrentDictionary>(); - public string Version { get; } + public string Version { get; } - public Task? ReloadPackagesTask { get; set; } + public Task? ReloadPackagesTask { get; set; } - // these properties will be set during host startup - public NexusProject Project { get; set; } = default!; + // these properties will be set during host startup + public NexusProject Project { get; set; } = default!; - public CatalogState CatalogState { get; set; } = default!; + public CatalogState CatalogState { get; set; } = default!; - public List DataWriterDescriptions { get; set; } = default!; + public List DataWriterDescriptions { get; set; } = default!; - #endregion - } + #endregion } diff --git a/src/Nexus/Core/CacheEntryWrapper.cs b/src/Nexus/Core/CacheEntryWrapper.cs index 78d471d2..7571533e 100644 --- a/src/Nexus/Core/CacheEntryWrapper.cs +++ b/src/Nexus/Core/CacheEntryWrapper.cs @@ -1,254 +1,253 @@ using Nexus.Utilities; -namespace Nexus.Core +namespace Nexus.Core; + +internal class CacheEntryWrapper : IDisposable { - internal class CacheEntryWrapper : IDisposable - { - private readonly DateTime _fileBegin; - private readonly TimeSpan _filePeriod; - private readonly TimeSpan _samplePeriod; - private readonly Stream _stream; + private readonly DateTime _fileBegin; + private readonly TimeSpan _filePeriod; + private readonly TimeSpan _samplePeriod; + private readonly Stream _stream; - private readonly long _dataSectionLength; + private readonly long _dataSectionLength; - private Interval[] _cachedIntervals; + private Interval[] _cachedIntervals; - public CacheEntryWrapper(DateTime fileBegin, TimeSpan filePeriod, TimeSpan samplePeriod, Stream stream) - { - _fileBegin = fileBegin; - _filePeriod = filePeriod; - _samplePeriod = samplePeriod; - _stream = stream; + public CacheEntryWrapper(DateTime fileBegin, TimeSpan filePeriod, TimeSpan samplePeriod, Stream stream) + { + _fileBegin = fileBegin; + _filePeriod = filePeriod; + _samplePeriod = samplePeriod; + _stream = stream; - var elementCount = filePeriod.Ticks / samplePeriod.Ticks; - _dataSectionLength = elementCount * sizeof(double); + var elementCount = filePeriod.Ticks / samplePeriod.Ticks; + _dataSectionLength = elementCount * sizeof(double); - // ensure a minimum length of data section + 1 x PeriodOfTime entry - if (_stream.Length == 0) - _stream.SetLength(_dataSectionLength + 1 + 2 * sizeof(long)); + // ensure a minimum length of data section + 1 x PeriodOfTime entry + if (_stream.Length == 0) + _stream.SetLength(_dataSectionLength + 1 + 2 * sizeof(long)); - // read cached periods - _stream.Seek(_dataSectionLength, SeekOrigin.Begin); - _cachedIntervals = ReadCachedIntervals(_stream); - } + // read cached periods + _stream.Seek(_dataSectionLength, SeekOrigin.Begin); + _cachedIntervals = ReadCachedIntervals(_stream); + } - public async Task ReadAsync( - DateTime begin, - DateTime end, - Memory targetBuffer, - CancellationToken cancellationToken) + public async Task ReadAsync( + DateTime begin, + DateTime end, + Memory targetBuffer, + CancellationToken cancellationToken) + { + /* + * _____ + * | | + * |___|__ end _________________ + * | | uncached period 3 + * | | + * | | _________________ + * |xxx| cached period 2 + * |xxx| + * |xxx| _________________ + * | | uncached period 2 + * | | _________________ + * |xxx| cached period 1 + * |xxx| _________________ + * | | uncached period 1 + * |___|__ begin _________________ + * | | + * |___|__ file begin + * + */ + + var index = 0; + var currentBegin = begin; + var uncachedIntervals = new List(); + + var isCachePeriod = false; + var isFirstIteration = true; + + while (currentBegin < end) { - /* - * _____ - * | | - * |___|__ end _________________ - * | | uncached period 3 - * | | - * | | _________________ - * |xxx| cached period 2 - * |xxx| - * |xxx| _________________ - * | | uncached period 2 - * | | _________________ - * |xxx| cached period 1 - * |xxx| _________________ - * | | uncached period 1 - * |___|__ begin _________________ - * | | - * |___|__ file begin - * - */ - - var index = 0; - var currentBegin = begin; - var uncachedIntervals = new List(); + var cachedInterval = index < _cachedIntervals.Length + ? _cachedIntervals[index] + : new Interval(DateTime.MaxValue, DateTime.MaxValue); - var isCachePeriod = false; - var isFirstIteration = true; + DateTime currentEnd; - while (currentBegin < end) + /* cached */ + if (cachedInterval.Begin <= currentBegin && currentBegin < cachedInterval.End) { - var cachedInterval = index < _cachedIntervals.Length - ? _cachedIntervals[index] - : new Interval(DateTime.MaxValue, DateTime.MaxValue); - - DateTime currentEnd; - - /* cached */ - if (cachedInterval.Begin <= currentBegin && currentBegin < cachedInterval.End) - { - currentEnd = new DateTime(Math.Min(cachedInterval.End.Ticks, end.Ticks), DateTimeKind.Utc); + currentEnd = new DateTime(Math.Min(cachedInterval.End.Ticks, end.Ticks), DateTimeKind.Utc); - var cacheOffset = NexusUtilities.Scale(currentBegin - _fileBegin, _samplePeriod); - var targetBufferOffset = NexusUtilities.Scale(currentBegin - begin, _samplePeriod); - var length = NexusUtilities.Scale(currentEnd - currentBegin, _samplePeriod); + var cacheOffset = NexusUtilities.Scale(currentBegin - _fileBegin, _samplePeriod); + var targetBufferOffset = NexusUtilities.Scale(currentBegin - begin, _samplePeriod); + var length = NexusUtilities.Scale(currentEnd - currentBegin, _samplePeriod); - var slicedTargetBuffer = targetBuffer.Slice(targetBufferOffset, length); - var slicedByteTargetBuffer = new CastMemoryManager(slicedTargetBuffer).Memory; + var slicedTargetBuffer = targetBuffer.Slice(targetBufferOffset, length); + var slicedByteTargetBuffer = new CastMemoryManager(slicedTargetBuffer).Memory; - _stream.Seek(cacheOffset * sizeof(double), SeekOrigin.Begin); - await _stream.ReadAsync(slicedByteTargetBuffer, cancellationToken); + _stream.Seek(cacheOffset * sizeof(double), SeekOrigin.Begin); + await _stream.ReadAsync(slicedByteTargetBuffer, cancellationToken); - if (currentEnd >= cachedInterval.End) - index++; + if (currentEnd >= cachedInterval.End) + index++; - if (!(isFirstIteration || isCachePeriod)) - uncachedIntervals[^1] = uncachedIntervals[^1] with { End = currentBegin }; + if (!(isFirstIteration || isCachePeriod)) + uncachedIntervals[^1] = uncachedIntervals[^1] with { End = currentBegin }; - isCachePeriod = true; - } - - /* uncached */ - else - { - currentEnd = new DateTime(Math.Min(cachedInterval.Begin.Ticks, end.Ticks), DateTimeKind.Utc); + isCachePeriod = true; + } - if (isFirstIteration || isCachePeriod) - uncachedIntervals.Add(new Interval(currentBegin, end)); + /* uncached */ + else + { + currentEnd = new DateTime(Math.Min(cachedInterval.Begin.Ticks, end.Ticks), DateTimeKind.Utc); - isCachePeriod = false; - } + if (isFirstIteration || isCachePeriod) + uncachedIntervals.Add(new Interval(currentBegin, end)); - isFirstIteration = false; - currentBegin = currentEnd; + isCachePeriod = false; } - return uncachedIntervals - .Where(period => (period.End - period.Begin) > TimeSpan.Zero) - .ToArray(); + isFirstIteration = false; + currentBegin = currentEnd; } - // https://www.geeksforgeeks.org/merging-intervals/ - class SortHelper : IComparer + return uncachedIntervals + .Where(period => (period.End - period.Begin) > TimeSpan.Zero) + .ToArray(); + } + + // https://www.geeksforgeeks.org/merging-intervals/ + class SortHelper : IComparer + { + public int Compare(Interval x, Interval y) { - public int Compare(Interval x, Interval y) - { - long result; + long result; - if (x.Begin == y.Begin) - result = x.End.Ticks - y.End.Ticks; + if (x.Begin == y.Begin) + result = x.End.Ticks - y.End.Ticks; - else - result = x.Begin.Ticks - y.Begin.Ticks; + else + result = x.Begin.Ticks - y.Begin.Ticks; - return result switch - { - < 0 => -1, - > 0 => +1, - _ => 0 - }; - } + return result switch + { + < 0 => -1, + > 0 => +1, + _ => 0 + }; } + } - public async Task WriteAsync( - DateTime begin, - Memory sourceBuffer, - CancellationToken cancellationToken) - { - var end = begin + _samplePeriod * sourceBuffer.Length; - var cacheOffset = NexusUtilities.Scale(begin - _fileBegin, _samplePeriod); - var byteSourceBuffer = new CastMemoryManager(sourceBuffer).Memory; + public async Task WriteAsync( + DateTime begin, + Memory sourceBuffer, + CancellationToken cancellationToken) + { + var end = begin + _samplePeriod * sourceBuffer.Length; + var cacheOffset = NexusUtilities.Scale(begin - _fileBegin, _samplePeriod); + var byteSourceBuffer = new CastMemoryManager(sourceBuffer).Memory; - _stream.Seek(cacheOffset * sizeof(double), SeekOrigin.Begin); - await _stream.WriteAsync(byteSourceBuffer, cancellationToken); + _stream.Seek(cacheOffset * sizeof(double), SeekOrigin.Begin); + await _stream.WriteAsync(byteSourceBuffer, cancellationToken); - /* update the list of cached intervals */ - var cachedIntervals = _cachedIntervals - .Concat(new[] { new Interval(begin, end) }) - .ToArray(); + /* update the list of cached intervals */ + var cachedIntervals = _cachedIntervals + .Concat(new[] { new Interval(begin, end) }) + .ToArray(); - /* merge list of intervals */ - if (cachedIntervals.Length > 1) - { - /* sort list of intervals */ - Array.Sort(cachedIntervals, new SortHelper()); + /* merge list of intervals */ + if (cachedIntervals.Length > 1) + { + /* sort list of intervals */ + Array.Sort(cachedIntervals, new SortHelper()); - /* stores index of last element */ - var index = 0; + /* stores index of last element */ + var index = 0; - for (int i = 1; i < cachedIntervals.Length; i++) + for (int i = 1; i < cachedIntervals.Length; i++) + { + /* if this is not first interval and overlaps with the previous one */ + if (cachedIntervals[index].End >= cachedIntervals[i].Begin) { - /* if this is not first interval and overlaps with the previous one */ - if (cachedIntervals[index].End >= cachedIntervals[i].Begin) + /* merge previous and current intervals */ + cachedIntervals[index] = cachedIntervals[index] with { - /* merge previous and current intervals */ - cachedIntervals[index] = cachedIntervals[index] with - { - End = new DateTime( - Math.Max( - cachedIntervals[index].End.Ticks, - cachedIntervals[i].End.Ticks), - DateTimeKind.Utc) - }; - } - - /* just add interval */ - else - { - index++; - cachedIntervals[index] = cachedIntervals[i]; - } + End = new DateTime( + Math.Max( + cachedIntervals[index].End.Ticks, + cachedIntervals[i].End.Ticks), + DateTimeKind.Utc) + }; } - _cachedIntervals = cachedIntervals - .Take(index + 1) - .ToArray(); - } - - else - { - _cachedIntervals = cachedIntervals; + /* just add interval */ + else + { + index++; + cachedIntervals[index] = cachedIntervals[i]; + } } - _stream.Seek(_dataSectionLength, SeekOrigin.Begin); - WriteCachedIntervals(_stream, _cachedIntervals); + _cachedIntervals = cachedIntervals + .Take(index + 1) + .ToArray(); } - public void Dispose() + else { - _stream.Dispose(); + _cachedIntervals = cachedIntervals; } - public static Interval[] ReadCachedIntervals(Stream stream) - { - var cachedPeriodCount = stream.ReadByte(); - var cachedIntervals = new Interval[cachedPeriodCount]; + _stream.Seek(_dataSectionLength, SeekOrigin.Begin); + WriteCachedIntervals(_stream, _cachedIntervals); + } - Span buffer = stackalloc byte[8]; + public void Dispose() + { + _stream.Dispose(); + } - for (int i = 0; i < cachedPeriodCount; i++) - { - stream.Read(buffer); - var beginTicks = BitConverter.ToInt64(buffer); + public static Interval[] ReadCachedIntervals(Stream stream) + { + var cachedPeriodCount = stream.ReadByte(); + var cachedIntervals = new Interval[cachedPeriodCount]; - stream.Read(buffer); - var endTicks = BitConverter.ToInt64(buffer); + Span buffer = stackalloc byte[8]; - cachedIntervals[i] = new Interval( - Begin: new DateTime(beginTicks, DateTimeKind.Utc), - End: new DateTime(endTicks, DateTimeKind.Utc)); - } + for (int i = 0; i < cachedPeriodCount; i++) + { + stream.Read(buffer); + var beginTicks = BitConverter.ToInt64(buffer); + + stream.Read(buffer); + var endTicks = BitConverter.ToInt64(buffer); - return cachedIntervals; + cachedIntervals[i] = new Interval( + Begin: new DateTime(beginTicks, DateTimeKind.Utc), + End: new DateTime(endTicks, DateTimeKind.Utc)); } - public static void WriteCachedIntervals(Stream stream, Interval[] cachedIntervals) - { - if (cachedIntervals.Length > byte.MaxValue) - throw new Exception("Only 256 cache periods per file are supported."); + return cachedIntervals; + } - stream.WriteByte((byte)cachedIntervals.Length); + public static void WriteCachedIntervals(Stream stream, Interval[] cachedIntervals) + { + if (cachedIntervals.Length > byte.MaxValue) + throw new Exception("Only 256 cache periods per file are supported."); - Span buffer = stackalloc byte[8]; + stream.WriteByte((byte)cachedIntervals.Length); - foreach (var cachedPeriod in cachedIntervals) - { - BitConverter.TryWriteBytes(buffer, cachedPeriod.Begin.Ticks); - stream.Write(buffer); + Span buffer = stackalloc byte[8]; - BitConverter.TryWriteBytes(buffer, cachedPeriod.End.Ticks); - stream.Write(buffer); - } + foreach (var cachedPeriod in cachedIntervals) + { + BitConverter.TryWriteBytes(buffer, cachedPeriod.Begin.Ticks); + stream.Write(buffer); + + BitConverter.TryWriteBytes(buffer, cachedPeriod.End.Ticks); + stream.Write(buffer); } } } diff --git a/src/Nexus/Core/CatalogCache.cs b/src/Nexus/Core/CatalogCache.cs index ae21aad4..464a590d 100644 --- a/src/Nexus/Core/CatalogCache.cs +++ b/src/Nexus/Core/CatalogCache.cs @@ -1,10 +1,9 @@ using Nexus.DataModel; using System.Collections.Concurrent; -namespace Nexus.Core +namespace Nexus.Core; + +internal class CatalogCache : ConcurrentDictionary> { - internal class CatalogCache : ConcurrentDictionary> - { - // This cache is required for DataSourceController.ReadAsync method to store original catalog items. - } + // This cache is required for DataSourceController.ReadAsync method to store original catalog items. } diff --git a/src/Nexus/Core/CatalogContainer.cs b/src/Nexus/Core/CatalogContainer.cs index 3e6eb5cf..1e342805 100644 --- a/src/Nexus/Core/CatalogContainer.cs +++ b/src/Nexus/Core/CatalogContainer.cs @@ -4,169 +4,168 @@ using System.Diagnostics; using System.Security.Claims; -namespace Nexus.Core +namespace Nexus.Core; + +[DebuggerDisplay("{Id,nq}")] +internal class CatalogContainer { - [DebuggerDisplay("{Id,nq}")] - internal class CatalogContainer + public const string RootCatalogId = "/"; + + private readonly SemaphoreSlim _semaphore = new(initialCount: 1, maxCount: 1); + private LazyCatalogInfo? _lazyCatalogInfo; + private CatalogContainer[]? _childCatalogContainers; + private readonly ICatalogManager _catalogManager; + private readonly IDatabaseService _databaseService; + private readonly IDataControllerService _dataControllerService; + + public CatalogContainer( + CatalogRegistration catalogRegistration, + ClaimsPrincipal? owner, + InternalDataSourceRegistration dataSourceRegistration, + InternalPackageReference packageReference, + CatalogMetadata metadata, + ICatalogManager catalogManager, + IDatabaseService databaseService, + IDataControllerService dataControllerService) { - public const string RootCatalogId = "/"; - - private readonly SemaphoreSlim _semaphore = new(initialCount: 1, maxCount: 1); - private LazyCatalogInfo? _lazyCatalogInfo; - private CatalogContainer[]? _childCatalogContainers; - private readonly ICatalogManager _catalogManager; - private readonly IDatabaseService _databaseService; - private readonly IDataControllerService _dataControllerService; - - public CatalogContainer( - CatalogRegistration catalogRegistration, - ClaimsPrincipal? owner, - InternalDataSourceRegistration dataSourceRegistration, - InternalPackageReference packageReference, - CatalogMetadata metadata, - ICatalogManager catalogManager, - IDatabaseService databaseService, - IDataControllerService dataControllerService) - { - Id = catalogRegistration.Path; - Title = catalogRegistration.Title; - IsTransient = catalogRegistration.IsTransient; - Owner = owner; - DataSourceRegistration = dataSourceRegistration; - PackageReference = packageReference; - Metadata = metadata; + Id = catalogRegistration.Path; + Title = catalogRegistration.Title; + IsTransient = catalogRegistration.IsTransient; + Owner = owner; + DataSourceRegistration = dataSourceRegistration; + PackageReference = packageReference; + Metadata = metadata; + + _catalogManager = catalogManager; + _databaseService = databaseService; + _dataControllerService = dataControllerService; + + if (owner is not null) + IsReleasable = AuthUtilities.IsCatalogWritable(Id, metadata, owner); + } - _catalogManager = catalogManager; - _databaseService = databaseService; - _dataControllerService = dataControllerService; + public string Id { get; } + public string? Title { get; } + public bool IsTransient { get; } - if (owner is not null) - IsReleasable = AuthUtilities.IsCatalogWritable(Id, metadata, owner); - } + public ClaimsPrincipal? Owner { get; } - public string Id { get; } - public string? Title { get; } - public bool IsTransient { get; } + public string PhysicalName => Id.TrimStart('/').Replace('/', '_'); - public ClaimsPrincipal? Owner { get; } + public InternalDataSourceRegistration DataSourceRegistration { get; } - public string PhysicalName => Id.TrimStart('/').Replace('/', '_'); + public InternalPackageReference PackageReference { get; } - public InternalDataSourceRegistration DataSourceRegistration { get; } + public CatalogMetadata Metadata { get; internal set; } - public InternalPackageReference PackageReference { get; } + public bool IsReleasable { get; } - public CatalogMetadata Metadata { get; internal set; } + public static CatalogContainer CreateRoot(ICatalogManager catalogManager, IDatabaseService databaseService) + { + return new CatalogContainer( + new CatalogRegistration(RootCatalogId, string.Empty), + default!, + default!, + default!, + default!, + catalogManager, + databaseService, default!); + } - public bool IsReleasable { get; } + public async Task> GetChildCatalogContainersAsync( + CancellationToken cancellationToken) + { + await _semaphore.WaitAsync(cancellationToken); - public static CatalogContainer CreateRoot(ICatalogManager catalogManager, IDatabaseService databaseService) + try { - return new CatalogContainer( - new CatalogRegistration(RootCatalogId, string.Empty), - default!, - default!, - default!, - default!, - catalogManager, - databaseService, default!); - } + if (IsTransient || _childCatalogContainers is null) + _childCatalogContainers = await _catalogManager.GetCatalogContainersAsync(this, cancellationToken); - public async Task> GetChildCatalogContainersAsync( - CancellationToken cancellationToken) + return _childCatalogContainers; + } + finally { - await _semaphore.WaitAsync(cancellationToken); - - try - { - if (IsTransient || _childCatalogContainers is null) - _childCatalogContainers = await _catalogManager.GetCatalogContainersAsync(this, cancellationToken); - - return _childCatalogContainers; - } - finally - { - _semaphore.Release(); - } + _semaphore.Release(); } + } - // TODO: Use Lazy instead? - public async Task GetLazyCatalogInfoAsync(CancellationToken cancellationToken) - { - await _semaphore.WaitAsync(cancellationToken); + // TODO: Use Lazy instead? + public async Task GetLazyCatalogInfoAsync(CancellationToken cancellationToken) + { + await _semaphore.WaitAsync(cancellationToken); - try - { - await EnsureLazyCatalogInfoAsync(cancellationToken); + try + { + await EnsureLazyCatalogInfoAsync(cancellationToken); - var lazyCatalogInfo = _lazyCatalogInfo; + var lazyCatalogInfo = _lazyCatalogInfo; - if (lazyCatalogInfo is null) - throw new Exception("this should never happen"); + if (lazyCatalogInfo is null) + throw new Exception("this should never happen"); - return lazyCatalogInfo; - } - finally - { - _semaphore.Release(); - } + return lazyCatalogInfo; + } + finally + { + _semaphore.Release(); } + } - public async Task UpdateMetadataAsync(CatalogMetadata metadata) + public async Task UpdateMetadataAsync(CatalogMetadata metadata) + { + await _semaphore.WaitAsync(); + + try { - await _semaphore.WaitAsync(); - - try - { - // persist - using var stream = _databaseService.WriteCatalogMetadata(Id); - await JsonSerializerHelper.SerializeIndentedAsync(stream, metadata); - - // assign - Metadata = metadata; - - // trigger merging of catalog and catalog overrides - _lazyCatalogInfo = default; - } - finally - { - _semaphore.Release(); - } + // persist + using var stream = _databaseService.WriteCatalogMetadata(Id); + await JsonSerializerHelper.SerializeIndentedAsync(stream, metadata); + + // assign + Metadata = metadata; + + // trigger merging of catalog and catalog overrides + _lazyCatalogInfo = default; + } + finally + { + _semaphore.Release(); } + } - private async Task EnsureLazyCatalogInfoAsync(CancellationToken cancellationToken) + private async Task EnsureLazyCatalogInfoAsync(CancellationToken cancellationToken) + { + if (IsTransient || _lazyCatalogInfo is null) { - if (IsTransient || _lazyCatalogInfo is null) - { - var catalogBegin = default(DateTime); - var catalogEnd = default(DateTime); + var catalogBegin = default(DateTime); + var catalogEnd = default(DateTime); - using var controller = await _dataControllerService.GetDataSourceControllerAsync(DataSourceRegistration, cancellationToken); - var catalog = await controller.GetCatalogAsync(Id, cancellationToken); + using var controller = await _dataControllerService.GetDataSourceControllerAsync(DataSourceRegistration, cancellationToken); + var catalog = await controller.GetCatalogAsync(Id, cancellationToken); - // get begin and end of project - var catalogTimeRange = await controller.GetTimeRangeAsync(catalog.Id, cancellationToken); + // get begin and end of project + var catalogTimeRange = await controller.GetTimeRangeAsync(catalog.Id, cancellationToken); - // merge time range - if (catalogBegin == DateTime.MinValue) - catalogBegin = catalogTimeRange.Begin; + // merge time range + if (catalogBegin == DateTime.MinValue) + catalogBegin = catalogTimeRange.Begin; - else - catalogBegin = new DateTime(Math.Min(catalogBegin.Ticks, catalogTimeRange.Begin.Ticks), DateTimeKind.Utc); + else + catalogBegin = new DateTime(Math.Min(catalogBegin.Ticks, catalogTimeRange.Begin.Ticks), DateTimeKind.Utc); - if (catalogEnd == DateTime.MinValue) - catalogEnd = catalogTimeRange.End; + if (catalogEnd == DateTime.MinValue) + catalogEnd = catalogTimeRange.End; - else - catalogEnd = new DateTime(Math.Max(catalogEnd.Ticks, catalogTimeRange.End.Ticks), DateTimeKind.Utc); + else + catalogEnd = new DateTime(Math.Max(catalogEnd.Ticks, catalogTimeRange.End.Ticks), DateTimeKind.Utc); - // merge catalog - if (Metadata?.Overrides is not null) - catalog = catalog.Merge(Metadata.Overrides); + // merge catalog + if (Metadata?.Overrides is not null) + catalog = catalog.Merge(Metadata.Overrides); - // - _lazyCatalogInfo = new LazyCatalogInfo(catalogBegin, catalogEnd, catalog); - } + // + _lazyCatalogInfo = new LazyCatalogInfo(catalogBegin, catalogEnd, catalog); } } } diff --git a/src/Nexus/Core/CatalogContainerExtensions.cs b/src/Nexus/Core/CatalogContainerExtensions.cs index 4f4e8879..f48dc44a 100644 --- a/src/Nexus/Core/CatalogContainerExtensions.cs +++ b/src/Nexus/Core/CatalogContainerExtensions.cs @@ -1,76 +1,75 @@ using Nexus.DataModel; -namespace Nexus.Core +namespace Nexus.Core; + +internal static class CatalogContainerExtensions { - internal static class CatalogContainerExtensions + public static async Task TryFindAsync( + this CatalogContainer parent, + string resourcePath, + CancellationToken cancellationToken) { - public static async Task TryFindAsync( - this CatalogContainer parent, - string resourcePath, - CancellationToken cancellationToken) - { - if (!DataModelUtilities.TryParseResourcePath(resourcePath, out var parseResult)) - throw new Exception("The resource path is malformed."); + if (!DataModelUtilities.TryParseResourcePath(resourcePath, out var parseResult)) + throw new Exception("The resource path is malformed."); - // find catalog - var catalogContainer = await parent.TryFindCatalogContainerAsync(parseResult.CatalogId, cancellationToken); + // find catalog + var catalogContainer = await parent.TryFindCatalogContainerAsync(parseResult.CatalogId, cancellationToken); - if (catalogContainer is null) - return default; + if (catalogContainer is null) + return default; - var lazyCatalogInfo = await catalogContainer.GetLazyCatalogInfoAsync(cancellationToken); - - if (lazyCatalogInfo is null) - return default; + var lazyCatalogInfo = await catalogContainer.GetLazyCatalogInfoAsync(cancellationToken); - // find base item - CatalogItem? catalogItem; - CatalogItem? baseCatalogItem = default; + if (lazyCatalogInfo is null) + return default; - if (parseResult.Kind == RepresentationKind.Original) - { - if (!lazyCatalogInfo.Catalog.TryFind(parseResult, out catalogItem)) - return default; - } + // find base item + CatalogItem? catalogItem; + CatalogItem? baseCatalogItem = default; - else - { - if (!lazyCatalogInfo.Catalog.TryFind(parseResult, out baseCatalogItem)) - return default; + if (parseResult.Kind == RepresentationKind.Original) + { + if (!lazyCatalogInfo.Catalog.TryFind(parseResult, out catalogItem)) + return default; + } - var representation = new Representation(NexusDataType.FLOAT64, parseResult.SamplePeriod, default, parseResult.Kind); + else + { + if (!lazyCatalogInfo.Catalog.TryFind(parseResult, out baseCatalogItem)) + return default; - catalogItem = baseCatalogItem with - { - Representation = representation - }; - } + var representation = new Representation(NexusDataType.FLOAT64, parseResult.SamplePeriod, default, parseResult.Kind); - return new CatalogItemRequest(catalogItem, baseCatalogItem, catalogContainer); + catalogItem = baseCatalogItem with + { + Representation = representation + }; } - public static async Task TryFindCatalogContainerAsync( - this CatalogContainer parent, - string catalogId, - CancellationToken cancellationToken) - { - var childCatalogContainers = await parent.GetChildCatalogContainersAsync(cancellationToken); - var catalogIdWithTrailingSlash = catalogId + "/"; /* the slashes are important to correctly find /A/D/E2 in the tests */ + return new CatalogItemRequest(catalogItem, baseCatalogItem, catalogContainer); + } - var catalogContainer = childCatalogContainers - .FirstOrDefault(current => catalogIdWithTrailingSlash.StartsWith(current.Id + "/")); + public static async Task TryFindCatalogContainerAsync( + this CatalogContainer parent, + string catalogId, + CancellationToken cancellationToken) + { + var childCatalogContainers = await parent.GetChildCatalogContainersAsync(cancellationToken); + var catalogIdWithTrailingSlash = catalogId + "/"; /* the slashes are important to correctly find /A/D/E2 in the tests */ - /* nothing found */ - if (catalogContainer is null) - return default; + var catalogContainer = childCatalogContainers + .FirstOrDefault(current => catalogIdWithTrailingSlash.StartsWith(current.Id + "/")); - /* catalogContainer is searched one */ - else if (catalogContainer.Id == catalogId) - return catalogContainer; + /* nothing found */ + if (catalogContainer is null) + return default; - /* catalogContainer is (grand)-parent of searched one */ - else - return await catalogContainer.TryFindCatalogContainerAsync(catalogId, cancellationToken); - } + /* catalogContainer is searched one */ + else if (catalogContainer.Id == catalogId) + return catalogContainer; + + /* catalogContainer is (grand)-parent of searched one */ + else + return await catalogContainer.TryFindCatalogContainerAsync(catalogId, cancellationToken); } } diff --git a/src/Nexus/Core/CustomExtensions.cs b/src/Nexus/Core/CustomExtensions.cs index af8a7b20..10aef6de 100644 --- a/src/Nexus/Core/CustomExtensions.cs +++ b/src/Nexus/Core/CustomExtensions.cs @@ -3,72 +3,71 @@ using System.Security.Cryptography; using System.Text; -namespace Nexus.Core +namespace Nexus.Core; + +internal static class CustomExtensions { - internal static class CustomExtensions - { #pragma warning disable VSTHRD200 // Verwenden Sie das Suffix "Async" f�r asynchrone Methoden - public static Task<(T[] Results, AggregateException Exception)> WhenAllEx(this IEnumerable> tasks) + public static Task<(T[] Results, AggregateException Exception)> WhenAllEx(this IEnumerable> tasks) #pragma warning restore VSTHRD200 // Verwenden Sie das Suffix "Async" f�r asynchrone Methoden + { + tasks = tasks.ToArray(); + + return Task.WhenAll(tasks).ContinueWith(t => { - tasks = tasks.ToArray(); + var results = tasks + .Where(task => task.Status == TaskStatus.RanToCompletion) + .Select(task => task.Result) + .ToArray(); - return Task.WhenAll(tasks).ContinueWith(t => - { - var results = tasks - .Where(task => task.Status == TaskStatus.RanToCompletion) - .Select(task => task.Result) + var aggregateExceptions = tasks + .Where(task => task.IsFaulted && task.Exception is not null) + .Select(task => task.Exception!) .ToArray(); - var aggregateExceptions = tasks - .Where(task => task.IsFaulted && task.Exception is not null) - .Select(task => task.Exception!) - .ToArray(); - - var flattenedAggregateException = new AggregateException(aggregateExceptions).Flatten(); + var flattenedAggregateException = new AggregateException(aggregateExceptions).Flatten(); - return (results, flattenedAggregateException); - }, default, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); - } + return (results, flattenedAggregateException); + }, default, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); + } - public static byte[] Hash(this string value) - { - var md5 = MD5.Create(); // compute hash is not thread safe! - var hash = md5.ComputeHash(Encoding.UTF8.GetBytes(value)); // - return hash; - } + public static byte[] Hash(this string value) + { + var md5 = MD5.Create(); // compute hash is not thread safe! + var hash = md5.ComputeHash(Encoding.UTF8.GetBytes(value)); // + return hash; + } - public static Memory Cast(this Memory buffer) - where TFrom : unmanaged - where To : unmanaged - { - return new CastMemoryManager(buffer).Memory; - } + public static Memory Cast(this Memory buffer) + where TFrom : unmanaged + where To : unmanaged + { + return new CastMemoryManager(buffer).Memory; + } - public static ReadOnlyMemory Cast(this ReadOnlyMemory buffer) - where TFrom : unmanaged - where To : unmanaged - { - return new CastMemoryManager(MemoryMarshal.AsMemory(buffer)).Memory; - } + public static ReadOnlyMemory Cast(this ReadOnlyMemory buffer) + where TFrom : unmanaged + where To : unmanaged + { + return new CastMemoryManager(MemoryMarshal.AsMemory(buffer)).Memory; + } - public static DateTime RoundDown(this DateTime dateTime, TimeSpan timeSpan) - { - return new DateTime(dateTime.Ticks - (dateTime.Ticks % timeSpan.Ticks), dateTime.Kind); - } + public static DateTime RoundDown(this DateTime dateTime, TimeSpan timeSpan) + { + return new DateTime(dateTime.Ticks - (dateTime.Ticks % timeSpan.Ticks), dateTime.Kind); + } - public static DateTime RoundUp(this DateTime dateTime, TimeSpan timeSpan) - { - var remainder = dateTime.Ticks % timeSpan.Ticks; + public static DateTime RoundUp(this DateTime dateTime, TimeSpan timeSpan) + { + var remainder = dateTime.Ticks % timeSpan.Ticks; - return remainder == 0 - ? dateTime - : dateTime.AddTicks(timeSpan.Ticks - remainder); - } + return remainder == 0 + ? dateTime + : dateTime.AddTicks(timeSpan.Ticks - remainder); + } - public static TimeSpan RoundDown(this TimeSpan timeSpan1, TimeSpan timeSpan2) - { - return new TimeSpan(timeSpan1.Ticks - (timeSpan1.Ticks % timeSpan2.Ticks)); - } + public static TimeSpan RoundDown(this TimeSpan timeSpan1, TimeSpan timeSpan2) + { + return new TimeSpan(timeSpan1.Ticks - (timeSpan1.Ticks % timeSpan2.Ticks)); } } diff --git a/src/Nexus/Core/InternalControllerFeatureProvider.cs b/src/Nexus/Core/InternalControllerFeatureProvider.cs index c7c0205e..1a603948 100644 --- a/src/Nexus/Core/InternalControllerFeatureProvider.cs +++ b/src/Nexus/Core/InternalControllerFeatureProvider.cs @@ -3,45 +3,44 @@ using Microsoft.AspNetCore.Mvc.Controllers; using System.Reflection; -namespace Nexus.Core +namespace Nexus.Core; + +internal class InternalControllerFeatureProvider : IApplicationFeatureProvider { - internal class InternalControllerFeatureProvider : IApplicationFeatureProvider - { - private const string ControllerTypeNameSuffix = "Controller"; + private const string ControllerTypeNameSuffix = "Controller"; - public void PopulateFeature(IEnumerable parts, ControllerFeature feature) + public void PopulateFeature(IEnumerable parts, ControllerFeature feature) + { + foreach (var part in parts.OfType()) { - foreach (var part in parts.OfType()) + foreach (var type in part.Types) { - foreach (var type in part.Types) + if (IsController(type) && !feature.Controllers.Contains(type)) { - if (IsController(type) && !feature.Controllers.Contains(type)) - { - feature.Controllers.Add(type); - } + feature.Controllers.Add(type); } } } + } - protected virtual bool IsController(TypeInfo typeInfo) - { - if (!typeInfo.IsClass) - return false; + protected virtual bool IsController(TypeInfo typeInfo) + { + if (!typeInfo.IsClass) + return false; - if (typeInfo.IsAbstract) - return false; + if (typeInfo.IsAbstract) + return false; - if (typeInfo.ContainsGenericParameters) - return false; + if (typeInfo.ContainsGenericParameters) + return false; - if (typeInfo.IsDefined(typeof(NonControllerAttribute))) - return false; + if (typeInfo.IsDefined(typeof(NonControllerAttribute))) + return false; - if (!typeInfo.Name.EndsWith(ControllerTypeNameSuffix, StringComparison.OrdinalIgnoreCase) && - !typeInfo.IsDefined(typeof(ControllerAttribute))) - return false; + if (!typeInfo.Name.EndsWith(ControllerTypeNameSuffix, StringComparison.OrdinalIgnoreCase) && + !typeInfo.IsDefined(typeof(ControllerAttribute))) + return false; - return true; - } + return true; } } diff --git a/src/Nexus/Core/LoggerExtensions.cs b/src/Nexus/Core/LoggerExtensions.cs index 2ba70ff1..b061c6c7 100644 --- a/src/Nexus/Core/LoggerExtensions.cs +++ b/src/Nexus/Core/LoggerExtensions.cs @@ -1,21 +1,20 @@ -namespace Nexus.Core +namespace Nexus.Core; + +internal static class LoggerExtensions { - internal static class LoggerExtensions + public static IDisposable + BeginNamedScope(this ILogger logger, string name, params ValueTuple[] stateProperties) { - public static IDisposable - BeginNamedScope(this ILogger logger, string name, params ValueTuple[] stateProperties) - { - var dictionary = stateProperties.ToDictionary(entry => entry.Item1, entry => entry.Item2); - dictionary[name + "_scope"] = Guid.NewGuid(); - return logger.BeginScope(dictionary) ?? throw new Exception("The scope is null."); - } + var dictionary = stateProperties.ToDictionary(entry => entry.Item1, entry => entry.Item2); + dictionary[name + "_scope"] = Guid.NewGuid(); + return logger.BeginScope(dictionary) ?? throw new Exception("The scope is null."); + } - public static IDisposable - BeginNamedScope(this ILogger logger, string name, IDictionary stateProperties) - { - var dictionary = stateProperties; - dictionary[name + "_scope"] = Guid.NewGuid(); - return logger.BeginScope(dictionary) ?? throw new Exception("The scope is null."); - } + public static IDisposable + BeginNamedScope(this ILogger logger, string name, IDictionary stateProperties) + { + var dictionary = stateProperties; + dictionary[name + "_scope"] = Guid.NewGuid(); + return logger.BeginScope(dictionary) ?? throw new Exception("The scope is null."); } } diff --git a/src/Nexus/Core/Models_NonPublic.cs b/src/Nexus/Core/Models_NonPublic.cs index 6670bdd9..018902b2 100644 --- a/src/Nexus/Core/Models_NonPublic.cs +++ b/src/Nexus/Core/Models_NonPublic.cs @@ -3,71 +3,70 @@ using System.IO.Pipelines; using System.Text.Json; -namespace Nexus.Core -{ - internal record InternalPersonalAccessToken( - Guid Id, - string Description, - DateTime Expires, - IReadOnlyList Claims); +namespace Nexus.Core; - internal record struct Interval( - DateTime Begin, - DateTime End); +internal record InternalPersonalAccessToken( + Guid Id, + string Description, + DateTime Expires, + IReadOnlyList Claims); - internal record ReadUnit( - CatalogItemRequest CatalogItemRequest, - PipeWriter DataWriter); +internal record struct Interval( + DateTime Begin, + DateTime End); - internal record CatalogItemRequest( - CatalogItem Item, - CatalogItem? BaseItem, - CatalogContainer Container); +internal record ReadUnit( + CatalogItemRequest CatalogItemRequest, + PipeWriter DataWriter); - internal record NexusProject( - IReadOnlyDictionary? SystemConfiguration, - IReadOnlyDictionary PackageReferences, - IReadOnlyDictionary UserConfigurations); +internal record CatalogItemRequest( + CatalogItem Item, + CatalogItem? BaseItem, + CatalogContainer Container); - internal record UserConfiguration( - IReadOnlyDictionary DataSourceRegistrations); +internal record NexusProject( + IReadOnlyDictionary? SystemConfiguration, + IReadOnlyDictionary PackageReferences, + IReadOnlyDictionary UserConfigurations); - internal record CatalogState( - CatalogContainer Root, - CatalogCache Cache); +internal record UserConfiguration( + IReadOnlyDictionary DataSourceRegistrations); - internal record LazyCatalogInfo( - DateTime Begin, - DateTime End, - ResourceCatalog Catalog); +internal record CatalogState( + CatalogContainer Root, + CatalogCache Cache); - internal record ExportContext( - TimeSpan SamplePeriod, - IEnumerable CatalogItemRequests, - ReadDataHandler ReadDataHandler, - ExportParameters ExportParameters); +internal record LazyCatalogInfo( + DateTime Begin, + DateTime End, + ResourceCatalog Catalog); - internal record JobControl( - DateTime Start, - Job Job, - CancellationTokenSource CancellationTokenSource) - { - public event EventHandler? ProgressUpdated; - public event EventHandler? Completed; +internal record ExportContext( + TimeSpan SamplePeriod, + IEnumerable CatalogItemRequests, + ReadDataHandler ReadDataHandler, + ExportParameters ExportParameters); - public double Progress { get; private set; } +internal record JobControl( + DateTime Start, + Job Job, + CancellationTokenSource CancellationTokenSource) +{ + public event EventHandler? ProgressUpdated; + public event EventHandler? Completed; - public Task Task { get; set; } = default!; + public double Progress { get; private set; } - public void OnProgressUpdated(double e) - { - Progress = e; - ProgressUpdated?.Invoke(this, e); - } + public Task Task { get; set; } = default!; - public void OnCompleted() - { - Completed?.Invoke(this, EventArgs.Empty); - } + public void OnProgressUpdated(double e) + { + Progress = e; + ProgressUpdated?.Invoke(this, e); + } + + public void OnCompleted() + { + Completed?.Invoke(this, EventArgs.Empty); } } diff --git a/src/Nexus/Core/Models_Public.cs b/src/Nexus/Core/Models_Public.cs index 9b3e80cf..b4c0c887 100644 --- a/src/Nexus/Core/Models_Public.cs +++ b/src/Nexus/Core/Models_Public.cs @@ -3,289 +3,288 @@ using System.Text.Json; using System.Text.Json.Serialization; -namespace Nexus.Core +namespace Nexus.Core; + +/// +/// Represents a user. +/// +public class NexusUser { - /// - /// Represents a user. - /// - public class NexusUser - { #pragma warning disable CS1591 - public NexusUser( - string id, - string name) - { - Id = id; - Name = name; - - Claims = new(); - } - - [JsonIgnore] - [ValidateNever] - public string Id { get; set; } = default!; - -#pragma warning restore CS1591 - - /// - /// The user name. - /// - public string Name { get; set; } = default!; + public NexusUser( + string id, + string name) + { + Id = id; + Name = name; -#pragma warning disable CS1591 + Claims = new(); + } - [JsonIgnore] - public List Claims { get; set; } = default!; + [JsonIgnore] + [ValidateNever] + public string Id { get; set; } = default!; #pragma warning restore CS1591 - } - /// - /// Represents a claim. + /// The user name. /// - public class NexusClaim - { -#pragma warning disable CS1591 + public string Name { get; set; } = default!; - public NexusClaim(Guid id, string type, string value) - { - Id = id; - Type = type; - Value = value; - } +#pragma warning disable CS1591 - [JsonIgnore] - [ValidateNever] - public Guid Id { get; set; } + [JsonIgnore] + public List Claims { get; set; } = default!; #pragma warning restore CS1591 - /// - /// The claim type. - /// - public string Type { get; init; } - - /// - /// The claim value. - /// - public string Value { get; init; } +} +/// +/// Represents a claim. +/// +public class NexusClaim +{ #pragma warning disable CS1591 - // https://learn.microsoft.com/en-us/ef/core/modeling/relationships?tabs=fluent-api%2Cfluent-api-simple-key%2Csimple-key#no-foreign-key-property - [JsonIgnore] - [ValidateNever] - public NexusUser Owner { get; set; } = default!; - -#pragma warning restore CS1591 + public NexusClaim(Guid id, string type, string value) + { + Id = id; + Type = type; + Value = value; } - /// - /// A personal access token. - /// - /// The token description. - /// The date/time when the token expires. - /// The claims that will be part of the token. - public record PersonalAccessToken( - string Description, - DateTime Expires, - IReadOnlyList Claims - ); - - /// - /// A revoke token request. - /// - /// The claim type. - /// The claim value. - public record TokenClaim( - string Type, - string Value); - - /// - /// Describes an OpenID connect provider. - /// - /// The scheme. - /// The display name. - public record AuthenticationSchemeDescription( - string Scheme, - string DisplayName); - - /// - /// A package reference. - /// - /// The provider which loads the package. - /// The configuration of the package reference. - public record PackageReference( - string Provider, - Dictionary Configuration); - - /* Required to workaround JsonIgnore problems with local serialization and OpenAPI. */ - internal record InternalPackageReference( - Guid Id, - string Provider, - Dictionary Configuration); - - /// - /// A structure for export parameters. - /// - /// The start date/time. - /// The end date/time. - /// The file period. - /// The writer type. If null, data will be read (and possibly cached) but not returned. This is useful for data pre-aggregation. - /// The resource paths to export. - /// The configuration. - public record ExportParameters( - DateTime Begin, - DateTime End, - TimeSpan FilePeriod, - string? Type, - string[] ResourcePaths, - IReadOnlyDictionary? Configuration); + [JsonIgnore] + [ValidateNever] + public Guid Id { get; set; } - /// - /// An extension description. - /// - /// The extension type. - /// The extension version. - /// A nullable description. - /// A nullable project website URL. - /// A nullable source repository URL. - /// Additional information about the extension. - public record ExtensionDescription( - string Type, - string Version, - string? Description, - string? ProjectUrl, - string? RepositoryUrl, - IReadOnlyDictionary? AdditionalInformation); - - /// - /// A structure for catalog information. - /// - /// The identifier. - /// A nullable title. - /// A nullable contact. - /// A nullable readme. - /// A nullable license. - /// A boolean which indicates if the catalog is accessible. - /// A boolean which indicates if the catalog is editable. - /// A boolean which indicates if the catalog is released. - /// A boolean which indicates if the catalog is visible. - /// A boolean which indicates if the catalog is owned by the current user. - /// A nullable info URL of the data source. - /// The data source type. - /// The data source registration identifier. - /// The package reference identifier. - public record CatalogInfo( - string Id, - string? Title, - string? Contact, - string? Readme, - string? License, - bool IsReadable, - bool IsWritable, - bool IsReleased, - bool IsVisible, - bool IsOwner, - string? DataSourceInfoUrl, - string DataSourceType, - Guid DataSourceRegistrationId, - Guid PackageReferenceId); - - /// - /// A structure for catalog metadata. - /// - /// The contact. - /// A list of groups the catalog is part of. - /// Overrides for the catalog. - public record CatalogMetadata( - string? Contact, - string[]? GroupMemberships, - ResourceCatalog? Overrides); - - /// - /// A catalog time range. - /// - /// The date/time of the first data in the catalog. - /// The date/time of the last data in the catalog. - public record CatalogTimeRange( - DateTime Begin, - DateTime End); +#pragma warning restore CS1591 /// - /// The catalog availability. + /// The claim type. /// - /// The actual availability data. - public record CatalogAvailability( - double[] Data); + public string Type { get; init; } /// - /// A data source registration. + /// The claim value. /// - /// The type of the data source. - /// An optional URL which points to the data. - /// Configuration parameters for the instantiated source. - /// An optional info URL. - /// An optional regular expressions pattern to select the catalogs to be released. By default, all catalogs will be released. - /// An optional regular expressions pattern to select the catalogs to be visible. By default, all catalogs will be visible. - public record DataSourceRegistration( - string Type, - Uri? ResourceLocator, - IReadOnlyDictionary? Configuration, - string? InfoUrl = default, - string? ReleasePattern = default, - string? VisibilityPattern = default); - - /* Required to workaround JsonIgnore problems with local serialization and OpenAPI. */ - internal record InternalDataSourceRegistration( - [property: JsonIgnore] Guid Id, - string Type, - Uri? ResourceLocator, - IReadOnlyDictionary? Configuration, - string? InfoUrl = default, - string? ReleasePattern = default, - string? VisibilityPattern = default); + public string Value { get; init; } - /// - /// Description of a job. - /// - /// The global unique identifier. - /// The owner of the job. - /// The job type. - /// The job parameters. - public record Job( - Guid Id, - string Type, - string Owner, - object? Parameters); +#pragma warning disable CS1591 - /// - /// Describes the status of the job. - /// - /// The start date/time. - /// The status. - /// The progress from 0 to 1. - /// The nullable exception message. - /// The nullable result. - public record JobStatus( - DateTime Start, - TaskStatus Status, - double Progress, - string? ExceptionMessage, - object? Result); + // https://learn.microsoft.com/en-us/ef/core/modeling/relationships?tabs=fluent-api%2Cfluent-api-simple-key%2Csimple-key#no-foreign-key-property + [JsonIgnore] + [ValidateNever] + public NexusUser Owner { get; set; } = default!; - /// - /// A me response. - /// - /// The user id. - /// The user. - /// A boolean which indicates if the user is an administrator. - /// A list of personal access tokens. - public record MeResponse( - string UserId, - NexusUser User, - bool IsAdmin, - IReadOnlyDictionary PersonalAccessTokens); +#pragma warning restore CS1591 } + +/// +/// A personal access token. +/// +/// The token description. +/// The date/time when the token expires. +/// The claims that will be part of the token. +public record PersonalAccessToken( + string Description, + DateTime Expires, + IReadOnlyList Claims +); + +/// +/// A revoke token request. +/// +/// The claim type. +/// The claim value. +public record TokenClaim( + string Type, + string Value); + +/// +/// Describes an OpenID connect provider. +/// +/// The scheme. +/// The display name. +public record AuthenticationSchemeDescription( + string Scheme, + string DisplayName); + +/// +/// A package reference. +/// +/// The provider which loads the package. +/// The configuration of the package reference. +public record PackageReference( + string Provider, + Dictionary Configuration); + +/* Required to workaround JsonIgnore problems with local serialization and OpenAPI. */ +internal record InternalPackageReference( + Guid Id, + string Provider, + Dictionary Configuration); + +/// +/// A structure for export parameters. +/// +/// The start date/time. +/// The end date/time. +/// The file period. +/// The writer type. If null, data will be read (and possibly cached) but not returned. This is useful for data pre-aggregation. +/// The resource paths to export. +/// The configuration. +public record ExportParameters( + DateTime Begin, + DateTime End, + TimeSpan FilePeriod, + string? Type, + string[] ResourcePaths, + IReadOnlyDictionary? Configuration); + +/// +/// An extension description. +/// +/// The extension type. +/// The extension version. +/// A nullable description. +/// A nullable project website URL. +/// A nullable source repository URL. +/// Additional information about the extension. +public record ExtensionDescription( + string Type, + string Version, + string? Description, + string? ProjectUrl, + string? RepositoryUrl, + IReadOnlyDictionary? AdditionalInformation); + +/// +/// A structure for catalog information. +/// +/// The identifier. +/// A nullable title. +/// A nullable contact. +/// A nullable readme. +/// A nullable license. +/// A boolean which indicates if the catalog is accessible. +/// A boolean which indicates if the catalog is editable. +/// A boolean which indicates if the catalog is released. +/// A boolean which indicates if the catalog is visible. +/// A boolean which indicates if the catalog is owned by the current user. +/// A nullable info URL of the data source. +/// The data source type. +/// The data source registration identifier. +/// The package reference identifier. +public record CatalogInfo( + string Id, + string? Title, + string? Contact, + string? Readme, + string? License, + bool IsReadable, + bool IsWritable, + bool IsReleased, + bool IsVisible, + bool IsOwner, + string? DataSourceInfoUrl, + string DataSourceType, + Guid DataSourceRegistrationId, + Guid PackageReferenceId); + +/// +/// A structure for catalog metadata. +/// +/// The contact. +/// A list of groups the catalog is part of. +/// Overrides for the catalog. +public record CatalogMetadata( + string? Contact, + string[]? GroupMemberships, + ResourceCatalog? Overrides); + +/// +/// A catalog time range. +/// +/// The date/time of the first data in the catalog. +/// The date/time of the last data in the catalog. +public record CatalogTimeRange( + DateTime Begin, + DateTime End); + +/// +/// The catalog availability. +/// +/// The actual availability data. +public record CatalogAvailability( + double[] Data); + +/// +/// A data source registration. +/// +/// The type of the data source. +/// An optional URL which points to the data. +/// Configuration parameters for the instantiated source. +/// An optional info URL. +/// An optional regular expressions pattern to select the catalogs to be released. By default, all catalogs will be released. +/// An optional regular expressions pattern to select the catalogs to be visible. By default, all catalogs will be visible. +public record DataSourceRegistration( + string Type, + Uri? ResourceLocator, + IReadOnlyDictionary? Configuration, + string? InfoUrl = default, + string? ReleasePattern = default, + string? VisibilityPattern = default); + +/* Required to workaround JsonIgnore problems with local serialization and OpenAPI. */ +internal record InternalDataSourceRegistration( + [property: JsonIgnore] Guid Id, + string Type, + Uri? ResourceLocator, + IReadOnlyDictionary? Configuration, + string? InfoUrl = default, + string? ReleasePattern = default, + string? VisibilityPattern = default); + +/// +/// Description of a job. +/// +/// The global unique identifier. +/// The owner of the job. +/// The job type. +/// The job parameters. +public record Job( + Guid Id, + string Type, + string Owner, + object? Parameters); + +/// +/// Describes the status of the job. +/// +/// The start date/time. +/// The status. +/// The progress from 0 to 1. +/// The nullable exception message. +/// The nullable result. +public record JobStatus( + DateTime Start, + TaskStatus Status, + double Progress, + string? ExceptionMessage, + object? Result); + +/// +/// A me response. +/// +/// The user id. +/// The user. +/// A boolean which indicates if the user is an administrator. +/// A list of personal access tokens. +public record MeResponse( + string UserId, + NexusUser User, + bool IsAdmin, + IReadOnlyDictionary PersonalAccessTokens); diff --git a/src/Nexus/Core/NexusAuthExtensions.cs b/src/Nexus/Core/NexusAuthExtensions.cs index 4e1907ac..412bc378 100644 --- a/src/Nexus/Core/NexusAuthExtensions.cs +++ b/src/Nexus/Core/NexusAuthExtensions.cs @@ -12,222 +12,221 @@ using System.Security.Claims; using static OpenIddict.Abstractions.OpenIddictConstants; -namespace Microsoft.Extensions.DependencyInjection +namespace Microsoft.Extensions.DependencyInjection; + +internal static class NexusAuthExtensions { - internal static class NexusAuthExtensions + public static OpenIdConnectProvider DefaultProvider { get; } = new OpenIdConnectProvider() { - public static OpenIdConnectProvider DefaultProvider { get; } = new OpenIdConnectProvider() - { - Scheme = "nexus", - DisplayName = "Nexus", - Authority = NexusUtilities.DefaultBaseUrl, - ClientId = "nexus", - ClientSecret = "nexus-secret" - }; + Scheme = "nexus", + DisplayName = "Nexus", + Authority = NexusUtilities.DefaultBaseUrl, + ClientId = "nexus", + ClientSecret = "nexus-secret" + }; + + public static IServiceCollection AddNexusAuth( + this IServiceCollection services, + PathsOptions pathsOptions, + SecurityOptions securityOptions) + { + /* https://stackoverflow.com/a/52493428/1636629 */ - public static IServiceCollection AddNexusAuth( - this IServiceCollection services, - PathsOptions pathsOptions, - SecurityOptions securityOptions) - { - /* https://stackoverflow.com/a/52493428/1636629 */ + JsonWebTokenHandler.DefaultInboundClaimTypeMap.Clear(); - JsonWebTokenHandler.DefaultInboundClaimTypeMap.Clear(); + services.AddDataProtection() + .PersistKeysToFileSystem(new DirectoryInfo(Path.Combine(pathsOptions.Config, "data-protection-keys"))); - services.AddDataProtection() - .PersistKeysToFileSystem(new DirectoryInfo(Path.Combine(pathsOptions.Config, "data-protection-keys"))); + var builder = services - var builder = services + .AddAuthentication(options => + { + options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; + }) - .AddAuthentication(options => - { - options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; - }) + .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options => + { + options.ExpireTimeSpan = securityOptions.CookieLifetime; + options.SlidingExpiration = false; - .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options => + options.Events.OnRedirectToAccessDenied = context => { - options.ExpireTimeSpan = securityOptions.CookieLifetime; - options.SlidingExpiration = false; + context.Response.StatusCode = (int)HttpStatusCode.Forbidden; + return Task.CompletedTask; + }; + }) - options.Events.OnRedirectToAccessDenied = context => - { - context.Response.StatusCode = (int)HttpStatusCode.Forbidden; - return Task.CompletedTask; - }; - }) + .AddScheme( + PersonalAccessTokenAuthenticationDefaults.AuthenticationScheme, default); - .AddScheme( - PersonalAccessTokenAuthenticationDefaults.AuthenticationScheme, default); + var providers = securityOptions.OidcProviders.Any() + ? securityOptions.OidcProviders + : new List() { DefaultProvider }; - var providers = securityOptions.OidcProviders.Any() - ? securityOptions.OidcProviders - : new List() { DefaultProvider }; + foreach (var provider in providers) + { + if (provider.Scheme == CookieAuthenticationDefaults.AuthenticationScheme) + continue; - foreach (var provider in providers) + builder.AddOpenIdConnect(provider.Scheme, provider.DisplayName, options => { - if (provider.Scheme == CookieAuthenticationDefaults.AuthenticationScheme) - continue; + options.Authority = provider.Authority; + options.ClientId = provider.ClientId; + options.ClientSecret = provider.ClientSecret; - builder.AddOpenIdConnect(provider.Scheme, provider.DisplayName, options => - { - options.Authority = provider.Authority; - options.ClientId = provider.ClientId; - options.ClientSecret = provider.ClientSecret; + options.CallbackPath = $"/signin-oidc/{provider.Scheme}"; + options.SignedOutCallbackPath = $"/signout-oidc/{provider.Scheme}"; - options.CallbackPath = $"/signin-oidc/{provider.Scheme}"; - options.SignedOutCallbackPath = $"/signout-oidc/{provider.Scheme}"; + options.ResponseType = OpenIdConnectResponseType.Code; - options.ResponseType = OpenIdConnectResponseType.Code; + options.TokenValidationParameters.AuthenticationType = provider.Scheme; + options.TokenValidationParameters.NameClaimType = Claims.Name; + options.TokenValidationParameters.RoleClaimType = Claims.Role; - options.TokenValidationParameters.AuthenticationType = provider.Scheme; - options.TokenValidationParameters.NameClaimType = Claims.Name; - options.TokenValidationParameters.RoleClaimType = Claims.Role; + /* user info endpoint is contacted AFTER OnTokenValidated, which requires the name claim to be present */ + options.GetClaimsFromUserInfoEndpoint = false; - /* user info endpoint is contacted AFTER OnTokenValidated, which requires the name claim to be present */ - options.GetClaimsFromUserInfoEndpoint = false; + var environmentName = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"); - var environmentName = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"); + if (environmentName == "Development") + options.RequireHttpsMetadata = false; - if (environmentName == "Development") - options.RequireHttpsMetadata = false; - - options.Events = new OpenIdConnectEvents() + options.Events = new OpenIdConnectEvents() + { + OnTokenResponseReceived = context => { - OnTokenResponseReceived = context => + /* OIDC spec RECOMMENDS id_token_hint (= id_token) to be added when + * post_logout_redirect_url is specified + * (https://openid.net/specs/openid-connect-rpinitiated-1_0.html) + * + * To be able to provide that parameter the ID token must become + * part of the auth cookie. The /connect/logout endpoint in + * NexusIdentityProviderExtensions.cs is then getting that logout_hint + * query parameter automatically (this has been tested!). + * This parameter is then part of the httpContext.Request.Query dict. + * + * Why do we enable this when this is just recommended? Because newer + * version of Keycloak REQUIRE it, otherwise we get a + * "Missing parameters: id_token_hint" error. + * + * Problem is very large size (> 8 kB) of cookie when setting + * options.SaveTokens = true; because then ALL OIDC tokens are stored + * in the cookie then. + * + * Solution: https://github.com/dotnet/aspnetcore/issues/30016#issuecomment-786384559 + * + * Cookie size is ~3.9 kB now. Unprotected cookie size is 2.2 kB + * (https://stackoverflow.com/a/69047119/1636629) where 1 kB, or 50%, + * comes from the id_token. + */ + context.Properties!.StoreTokens(new[] { - /* OIDC spec RECOMMENDS id_token_hint (= id_token) to be added when - * post_logout_redirect_url is specified - * (https://openid.net/specs/openid-connect-rpinitiated-1_0.html) - * - * To be able to provide that parameter the ID token must become - * part of the auth cookie. The /connect/logout endpoint in - * NexusIdentityProviderExtensions.cs is then getting that logout_hint - * query parameter automatically (this has been tested!). - * This parameter is then part of the httpContext.Request.Query dict. - * - * Why do we enable this when this is just recommended? Because newer - * version of Keycloak REQUIRE it, otherwise we get a - * "Missing parameters: id_token_hint" error. - * - * Problem is very large size (> 8 kB) of cookie when setting - * options.SaveTokens = true; because then ALL OIDC tokens are stored - * in the cookie then. - * - * Solution: https://github.com/dotnet/aspnetcore/issues/30016#issuecomment-786384559 - * - * Cookie size is ~3.9 kB now. Unprotected cookie size is 2.2 kB - * (https://stackoverflow.com/a/69047119/1636629) where 1 kB, or 50%, - * comes from the id_token. - */ - context.Properties!.StoreTokens(new[] + new AuthenticationToken { - new AuthenticationToken - { - Name = "id_token", - Value = context.TokenEndpointResponse.IdToken - } - }); - - return Task.CompletedTask; - }, - - OnTokenValidated = async context => - { - // scopes - // https://openid.net/specs/openid-connect-basic-1_0.html#Scopes + Name = "id_token", + Value = context.TokenEndpointResponse.IdToken + } + }); - var principal = context.Principal; + return Task.CompletedTask; + }, - if (principal is null) - throw new Exception("The principal is null. This should never happen."); + OnTokenValidated = async context => + { + // scopes + // https://openid.net/specs/openid-connect-basic-1_0.html#Scopes - var userId = principal.FindFirstValue(Claims.Subject) - ?? throw new Exception("The subject claim is missing. This should never happen."); + var principal = context.Principal; - var username = principal.FindFirstValue(Claims.Name) - ?? throw new Exception("The name claim is required."); + if (principal is null) + throw new Exception("The principal is null. This should never happen."); - using var dbContext = context.HttpContext.RequestServices.GetRequiredService(); - var uniqueUserId = $"{Uri.EscapeDataString(userId)}@{Uri.EscapeDataString(context.Scheme.Name)}"; + var userId = principal.FindFirstValue(Claims.Subject) + ?? throw new Exception("The subject claim is missing. This should never happen."); - // user - var user = await dbContext.Users - .Include(user => user.Claims) - .SingleOrDefaultAsync(user => user.Id == uniqueUserId); + var username = principal.FindFirstValue(Claims.Name) + ?? throw new Exception("The name claim is required."); - if (user is null) - { - var newClaims = new List(); - var isFirstUser = !dbContext.Users.Any(); + using var dbContext = context.HttpContext.RequestServices.GetRequiredService(); + var uniqueUserId = $"{Uri.EscapeDataString(userId)}@{Uri.EscapeDataString(context.Scheme.Name)}"; - if (isFirstUser) - newClaims.Add(new NexusClaim(Guid.NewGuid(), Claims.Role, NexusRoles.ADMINISTRATOR)); + // user + var user = await dbContext.Users + .Include(user => user.Claims) + .SingleOrDefaultAsync(user => user.Id == uniqueUserId); - newClaims.Add(new NexusClaim(Guid.NewGuid(), Claims.Role, NexusRoles.USER)); + if (user is null) + { + var newClaims = new List(); + var isFirstUser = !dbContext.Users.Any(); - user = new NexusUser( - id: uniqueUserId, - name: username) - { - Claims = newClaims - }; + if (isFirstUser) + newClaims.Add(new NexusClaim(Guid.NewGuid(), Claims.Role, NexusRoles.ADMINISTRATOR)); - dbContext.Users.Add(user); - } + newClaims.Add(new NexusClaim(Guid.NewGuid(), Claims.Role, NexusRoles.USER)); - else + user = new NexusUser( + id: uniqueUserId, + name: username) { - // user name may change, so update it - user.Name = username; - } + Claims = newClaims + }; - await dbContext.SaveChangesAsync(); + dbContext.Users.Add(user); + } - // oidc identity - var oidcIdentity = (ClaimsIdentity)principal.Identity!; - var subClaim = oidcIdentity.FindFirst(Claims.Subject); + else + { + // user name may change, so update it + user.Name = username; + } - if (subClaim is not null) - oidcIdentity.RemoveClaim(subClaim); + await dbContext.SaveChangesAsync(); - oidcIdentity.AddClaim(new Claim(Claims.Subject, uniqueUserId)); + // oidc identity + var oidcIdentity = (ClaimsIdentity)principal.Identity!; + var subClaim = oidcIdentity.FindFirst(Claims.Subject); - // app identity - var claims = user.Claims.Select(entry => new Claim(entry.Type, entry.Value)); + if (subClaim is not null) + oidcIdentity.RemoveClaim(subClaim); - var appIdentity = new ClaimsIdentity( - claims, - authenticationType: context.Scheme.Name, - nameType: Claims.Name, - roleType: Claims.Role); + oidcIdentity.AddClaim(new Claim(Claims.Subject, uniqueUserId)); - principal.AddIdentity(appIdentity); - } - }; - }); - } + // app identity + var claims = user.Claims.Select(entry => new Claim(entry.Type, entry.Value)); - var authenticationSchemes = new[] - { - CookieAuthenticationDefaults.AuthenticationScheme, - PersonalAccessTokenAuthenticationDefaults.AuthenticationScheme - }; + var appIdentity = new ClaimsIdentity( + claims, + authenticationType: context.Scheme.Name, + nameType: Claims.Name, + roleType: Claims.Role); - services.AddAuthorization(options => - { - options.DefaultPolicy = new AuthorizationPolicyBuilder() - .RequireAuthenticatedUser() - .RequireRole(NexusRoles.USER) - .AddAuthenticationSchemes(authenticationSchemes) - .Build(); - - options - .AddPolicy(NexusPolicies.RequireAdmin, policy => policy - .RequireRole(NexusRoles.ADMINISTRATOR) - .AddAuthenticationSchemes(authenticationSchemes)); + principal.AddIdentity(appIdentity); + } + }; }); - - return services; } + + var authenticationSchemes = new[] + { + CookieAuthenticationDefaults.AuthenticationScheme, + PersonalAccessTokenAuthenticationDefaults.AuthenticationScheme + }; + + services.AddAuthorization(options => + { + options.DefaultPolicy = new AuthorizationPolicyBuilder() + .RequireAuthenticatedUser() + .RequireRole(NexusRoles.USER) + .AddAuthenticationSchemes(authenticationSchemes) + .Build(); + + options + .AddPolicy(NexusPolicies.RequireAdmin, policy => policy + .RequireRole(NexusRoles.ADMINISTRATOR) + .AddAuthenticationSchemes(authenticationSchemes)); + }); + + return services; } } diff --git a/src/Nexus/Core/NexusClaims.cs b/src/Nexus/Core/NexusClaims.cs index ae112b69..1cf738a5 100644 --- a/src/Nexus/Core/NexusClaims.cs +++ b/src/Nexus/Core/NexusClaims.cs @@ -1,15 +1,14 @@ -namespace Nexus.Core +namespace Nexus.Core; + +internal static class NexusClaims { - internal static class NexusClaims - { - public const string CAN_READ_CATALOG = "CanReadCatalog"; - public const string CAN_WRITE_CATALOG = "CanWriteCatalog"; - public const string CAN_READ_CATALOG_GROUP = "CanReadCatalogGroup"; - public const string CAN_WRITE_CATALOG_GROUP = "CanWriteCatalogGroup"; + public const string CAN_READ_CATALOG = "CanReadCatalog"; + public const string CAN_WRITE_CATALOG = "CanWriteCatalog"; + public const string CAN_READ_CATALOG_GROUP = "CanReadCatalogGroup"; + public const string CAN_WRITE_CATALOG_GROUP = "CanWriteCatalogGroup"; - public static string ToPatUserClaimType(string claimType) - { - return $"pat_user_{claimType}"; - } + public static string ToPatUserClaimType(string claimType) + { + return $"pat_user_{claimType}"; } } \ No newline at end of file diff --git a/src/Nexus/Core/NexusIdentityProviderExtensions.cs b/src/Nexus/Core/NexusIdentityProviderExtensions.cs index e6a63b2e..24697d6d 100644 --- a/src/Nexus/Core/NexusIdentityProviderExtensions.cs +++ b/src/Nexus/Core/NexusIdentityProviderExtensions.cs @@ -8,227 +8,226 @@ using System.Security.Claims; using static OpenIddict.Abstractions.OpenIddictConstants; -namespace Microsoft.Extensions.DependencyInjection +namespace Microsoft.Extensions.DependencyInjection; + +internal static class NexusIdentityProviderExtensions { - internal static class NexusIdentityProviderExtensions + public static IServiceCollection AddNexusIdentityProvider( + this IServiceCollection services) { - public static IServiceCollection AddNexusIdentityProvider( - this IServiceCollection services) + // entity framework + services.AddDbContext(options => { - // entity framework - services.AddDbContext(options => - { - options.UseInMemoryDatabase("OpenIddict"); - options.UseOpenIddict(); - }); - - // OpenIddict - services.AddOpenIddict() - - .AddCore(options => - { - options - .UseEntityFrameworkCore() - .UseDbContext(); - }) + options.UseInMemoryDatabase("OpenIddict"); + options.UseOpenIddict(); + }); - .AddServer(options => - { - options - .AllowAuthorizationCodeFlow() - .RequireProofKeyForCodeExchange(); - - options - .AddEphemeralEncryptionKey() - .AddEphemeralSigningKey(); - - options - .SetAuthorizationEndpointUris("/connect/authorize") - .SetTokenEndpointUris("/connect/token") - .SetLogoutEndpointUris("/connect/logout"); + // OpenIddict + services.AddOpenIddict() - options - .RegisterScopes( - Scopes.OpenId, - Scopes.Profile); - - var aspNetCoreBuilder = options - .UseAspNetCore() - .EnableAuthorizationEndpointPassthrough() - .EnableLogoutEndpointPassthrough() - .EnableTokenEndpointPassthrough(); - - var environmentName = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"); + .AddCore(options => + { + options + .UseEntityFrameworkCore() + .UseDbContext(); + }) - if (environmentName == "Development") - aspNetCoreBuilder.DisableTransportSecurityRequirement(); - }); + .AddServer(options => + { + options + .AllowAuthorizationCodeFlow() + .RequireProofKeyForCodeExchange(); + + options + .AddEphemeralEncryptionKey() + .AddEphemeralSigningKey(); + + options + .SetAuthorizationEndpointUris("/connect/authorize") + .SetTokenEndpointUris("/connect/token") + .SetLogoutEndpointUris("/connect/logout"); + + options + .RegisterScopes( + Scopes.OpenId, + Scopes.Profile); + + var aspNetCoreBuilder = options + .UseAspNetCore() + .EnableAuthorizationEndpointPassthrough() + .EnableLogoutEndpointPassthrough() + .EnableTokenEndpointPassthrough(); + + var environmentName = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"); + + if (environmentName == "Development") + aspNetCoreBuilder.DisableTransportSecurityRequirement(); + }); - services.AddHostedService(); + services.AddHostedService(); - return services; - } + return services; + } - public static WebApplication UseNexusIdentityProvider( - this WebApplication app) + public static WebApplication UseNexusIdentityProvider( + this WebApplication app) + { + // AuthorizationController.cs https://github.com/openiddict/openiddict-samples/blob/dev/samples/Balosar/Balosar.Server/Controllers/AuthorizationController.cs + app.MapGet("/connect/authorize", async ( + HttpContext httpContext, + [FromServices] IOpenIddictApplicationManager applicationManager, + [FromServices] IOpenIddictAuthorizationManager authorizationManager) => { - // AuthorizationController.cs https://github.com/openiddict/openiddict-samples/blob/dev/samples/Balosar/Balosar.Server/Controllers/AuthorizationController.cs - app.MapGet("/connect/authorize", async ( - HttpContext httpContext, - [FromServices] IOpenIddictApplicationManager applicationManager, - [FromServices] IOpenIddictAuthorizationManager authorizationManager) => - { - // request - var request = httpContext.GetOpenIddictServerRequest() ?? - throw new InvalidOperationException("The OpenID Connect request cannot be retrieved."); + // request + var request = httpContext.GetOpenIddictServerRequest() ?? + throw new InvalidOperationException("The OpenID Connect request cannot be retrieved."); - // client - var clientId = request.ClientId ?? string.Empty; + // client + var clientId = request.ClientId ?? string.Empty; - var client = await applicationManager.FindByClientIdAsync(clientId) ?? - throw new InvalidOperationException("Details concerning the calling client application cannot be found."); + var client = await applicationManager.FindByClientIdAsync(clientId) ?? + throw new InvalidOperationException("Details concerning the calling client application cannot be found."); - // subject - var subject = "f9208f50-cd54-4165-8041-b5cd19af45a4"; + // subject + var subject = "f9208f50-cd54-4165-8041-b5cd19af45a4"; - // principal - var claims = new[] - { - new Claim(Claims.Subject, subject), - new Claim(Claims.Name, "Star Lord"), - }; - - var principal = new ClaimsPrincipal( - new ClaimsIdentity( - claims, - authenticationType: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, - nameType: Claims.Name, - roleType: Claims.Role)); - - // authorization - var authorizationsEnumerable = authorizationManager.FindAsync( + // principal + var claims = new[] + { + new Claim(Claims.Subject, subject), + new Claim(Claims.Name, "Star Lord"), + }; + + var principal = new ClaimsPrincipal( + new ClaimsIdentity( + claims, + authenticationType: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, + nameType: Claims.Name, + roleType: Claims.Role)); + + // authorization + var authorizationsEnumerable = authorizationManager.FindAsync( + subject: subject, + client: (await applicationManager.GetIdAsync(client))!, + status: Statuses.Valid, + type: AuthorizationTypes.Permanent, + scopes: request.GetScopes()); + + var authorizations = new List(); + + await foreach (var current in authorizationsEnumerable) + authorizations.Add(current); + + var authorization = authorizations + .LastOrDefault(); + + authorization ??= await authorizationManager.CreateAsync( + principal: principal, subject: subject, client: (await applicationManager.GetIdAsync(client))!, - status: Statuses.Valid, type: AuthorizationTypes.Permanent, - scopes: request.GetScopes()); + scopes: principal.GetScopes()); - var authorizations = new List(); + principal.SetAuthorizationId(await authorizationManager.GetIdAsync(authorization)); - await foreach (var current in authorizationsEnumerable) - authorizations.Add(current); + // claims + foreach (var claim in principal.Claims) + { + claim.SetDestinations(Destinations.IdentityToken); + } - var authorization = authorizations - .LastOrDefault(); + return Results.SignIn(principal, authenticationScheme: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); + }); - authorization ??= await authorizationManager.CreateAsync( - principal: principal, - subject: subject, - client: (await applicationManager.GetIdAsync(client))!, - type: AuthorizationTypes.Permanent, - scopes: principal.GetScopes()); + // AuthorizationController.cs https://github.com/openiddict/openiddict-samples/blob/dev/samples/Balosar/Balosar.Server/Controllers/AuthorizationController.cs + app.MapPost("/connect/token", async ( + HttpContext httpContext) => + { + var request = httpContext.GetOpenIddictServerRequest() ?? + throw new InvalidOperationException("The OpenID Connect request cannot be retrieved."); - principal.SetAuthorizationId(await authorizationManager.GetIdAsync(authorization)); + if (request.IsAuthorizationCodeGrantType()) + { + var principal = (await httpContext + .AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)) + .Principal; - // claims - foreach (var claim in principal.Claims) + if (principal is null) { - claim.SetDestinations(Destinations.IdentityToken); + return Results.Forbid( + authenticationSchemes: new[] { OpenIddictServerAspNetCoreDefaults.AuthenticationScheme }, + properties: new AuthenticationProperties(new Dictionary + { + [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant, + [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The token is no longer valid." + })); } + // returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens. return Results.SignIn(principal, authenticationScheme: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); - }); + } - // AuthorizationController.cs https://github.com/openiddict/openiddict-samples/blob/dev/samples/Balosar/Balosar.Server/Controllers/AuthorizationController.cs - app.MapPost("/connect/token", async ( - HttpContext httpContext) => - { - var request = httpContext.GetOpenIddictServerRequest() ?? - throw new InvalidOperationException("The OpenID Connect request cannot be retrieved."); + throw new InvalidOperationException("The specified grant type is not supported."); + }); - if (request.IsAuthorizationCodeGrantType()) - { - var principal = (await httpContext - .AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)) - .Principal; - - if (principal is null) - { - return Results.Forbid( - authenticationSchemes: new[] { OpenIddictServerAspNetCoreDefaults.AuthenticationScheme }, - properties: new AuthenticationProperties(new Dictionary - { - [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant, - [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The token is no longer valid." - })); - } - - // returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens. - return Results.SignIn(principal, authenticationScheme: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); - } + app.MapGet("/connect/logout", ( + HttpContext httpContext) => + { + var redirectUrl = httpContext.Request.Query["post_logout_redirect_uri"]!.ToString(); + var state = httpContext.Request.Query["state"]!.ToString(); + return Results.Redirect(redirectUrl + $"?state={state}"); + }); - throw new InvalidOperationException("The specified grant type is not supported."); - }); + return app; + } +} - app.MapGet("/connect/logout", ( - HttpContext httpContext) => - { - var redirectUrl = httpContext.Request.Query["post_logout_redirect_uri"]!.ToString(); - var state = httpContext.Request.Query["state"]!.ToString(); - return Results.Redirect(redirectUrl + $"?state={state}"); - }); +internal class HostedService : IHostedService +{ + private readonly IServiceProvider _serviceProvider; - return app; - } + public HostedService(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; } - internal class HostedService : IHostedService + public async Task StartAsync(CancellationToken cancellationToken) { - private readonly IServiceProvider _serviceProvider; - - public HostedService(IServiceProvider serviceProvider) - { - _serviceProvider = serviceProvider; - } - - public async Task StartAsync(CancellationToken cancellationToken) - { - using var scope = _serviceProvider.CreateScope(); + using var scope = _serviceProvider.CreateScope(); - using var context = scope.ServiceProvider.GetRequiredService(); - await context.Database.EnsureCreatedAsync(cancellationToken); + using var context = scope.ServiceProvider.GetRequiredService(); + await context.Database.EnsureCreatedAsync(cancellationToken); - var manager = scope.ServiceProvider.GetRequiredService(); + var manager = scope.ServiceProvider.GetRequiredService(); - if (await manager.FindByClientIdAsync("nexus", cancellationToken) is null) + if (await manager.FindByClientIdAsync("nexus", cancellationToken) is null) + { + await manager.CreateAsync(new OpenIddictApplicationDescriptor { - await manager.CreateAsync(new OpenIddictApplicationDescriptor + ClientId = "nexus", + ClientSecret = "nexus-secret", + DisplayName = "Nexus", + RedirectUris = { new Uri($"{NexusUtilities.DefaultBaseUrl}/signin-oidc/nexus") }, + PostLogoutRedirectUris = { new Uri($"{NexusUtilities.DefaultBaseUrl}/signout-oidc/nexus") }, + Permissions = { - ClientId = "nexus", - ClientSecret = "nexus-secret", - DisplayName = "Nexus", - RedirectUris = { new Uri($"{NexusUtilities.DefaultBaseUrl}/signin-oidc/nexus") }, - PostLogoutRedirectUris = { new Uri($"{NexusUtilities.DefaultBaseUrl}/signout-oidc/nexus") }, - Permissions = - { - // endpoints - Permissions.Endpoints.Authorization, - Permissions.Endpoints.Token, - Permissions.Endpoints.Logout, - - // grant types - Permissions.GrantTypes.AuthorizationCode, - - // response types - Permissions.ResponseTypes.Code, - - // scopes - Permissions.Scopes.Profile - } - }, cancellationToken); - } + // endpoints + Permissions.Endpoints.Authorization, + Permissions.Endpoints.Token, + Permissions.Endpoints.Logout, + + // grant types + Permissions.GrantTypes.AuthorizationCode, + + // response types + Permissions.ResponseTypes.Code, + + // scopes + Permissions.Scopes.Profile + } + }, cancellationToken); } - - public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; } + + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; } diff --git a/src/Nexus/Core/NexusOpenApiExtensions.cs b/src/Nexus/Core/NexusOpenApiExtensions.cs index 24de9c36..417c4102 100644 --- a/src/Nexus/Core/NexusOpenApiExtensions.cs +++ b/src/Nexus/Core/NexusOpenApiExtensions.cs @@ -4,82 +4,81 @@ using NSwag.AspNetCore; using System.Text.Json.Serialization; -namespace Microsoft.Extensions.DependencyInjection +namespace Microsoft.Extensions.DependencyInjection; + +internal static class NexusOpenApiExtensions { - internal static class NexusOpenApiExtensions + public static IServiceCollection AddNexusOpenApi( + this IServiceCollection services) { - public static IServiceCollection AddNexusOpenApi( - this IServiceCollection services) - { - // https://github.com/dotnet/aspnet-api-versioning/tree/master/samples/aspnetcore/SwaggerSample - services - .AddControllers(options => options.InputFormatters.Add(new StreamInputFormatter())) - .AddJsonOptions(options => options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter())) - .ConfigureApplicationPartManager( - manager => - { - manager.FeatureProviders.Add(new InternalControllerFeatureProvider()); - }); - - services.AddApiVersioning( - options => - { - options.ReportApiVersions = true; - }); - - services.AddVersionedApiExplorer( - options => + // https://github.com/dotnet/aspnet-api-versioning/tree/master/samples/aspnetcore/SwaggerSample + services + .AddControllers(options => options.InputFormatters.Add(new StreamInputFormatter())) + .AddJsonOptions(options => options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter())) + .ConfigureApplicationPartManager( + manager => { - options.GroupNameFormat = "'v'VVV"; - options.SubstituteApiVersionInUrl = true; + manager.FeatureProviders.Add(new InternalControllerFeatureProvider()); }); - /* not optimal */ - var provider = services.BuildServiceProvider().GetRequiredService(); + services.AddApiVersioning( + options => + { + options.ReportApiVersions = true; + }); - foreach (var description in provider.ApiVersionDescriptions) + services.AddVersionedApiExplorer( + options => { - services.AddOpenApiDocument(config => - { - config.SchemaSettings.DefaultReferenceTypeNullHandling = ReferenceTypeNullHandling.NotNull; + options.GroupNameFormat = "'v'VVV"; + options.SubstituteApiVersionInUrl = true; + }); - config.Title = "Nexus REST API"; - config.Version = description.GroupName; - config.Description = "Explore resources and get their data." - + (description.IsDeprecated ? " This API version is deprecated." : ""); + /* not optimal */ + var provider = services.BuildServiceProvider().GetRequiredService(); - config.ApiGroupNames = new[] { description.GroupName }; - config.DocumentName = description.GroupName; - }); - } + foreach (var description in provider.ApiVersionDescriptions) + { + services.AddOpenApiDocument(config => + { + config.SchemaSettings.DefaultReferenceTypeNullHandling = ReferenceTypeNullHandling.NotNull; - return services; + config.Title = "Nexus REST API"; + config.Version = description.GroupName; + config.Description = "Explore resources and get their data." + + (description.IsDeprecated ? " This API version is deprecated." : ""); + + config.ApiGroupNames = new[] { description.GroupName }; + config.DocumentName = description.GroupName; + }); } - public static IApplicationBuilder UseNexusOpenApi( - this IApplicationBuilder app, - IApiVersionDescriptionProvider provider, - bool addExplorer) - { - app.UseOpenApi(settings => settings.Path = "/openapi/{documentName}/openapi.json"); + return services; + } - if (addExplorer) - { - app.UseSwaggerUi(settings => - { - settings.Path = "/api"; + public static IApplicationBuilder UseNexusOpenApi( + this IApplicationBuilder app, + IApiVersionDescriptionProvider provider, + bool addExplorer) + { + app.UseOpenApi(settings => settings.Path = "/openapi/{documentName}/openapi.json"); - foreach (var description in provider.ApiVersionDescriptions) - { - settings.SwaggerRoutes.Add( - new SwaggerUiRoute( - description.GroupName.ToUpperInvariant(), - $"/openapi/{description.GroupName}/openapi.json")); - } - }); - } + if (addExplorer) + { + app.UseSwaggerUi(settings => + { + settings.Path = "/api"; - return app; + foreach (var description in provider.ApiVersionDescriptions) + { + settings.SwaggerRoutes.Add( + new SwaggerUiRoute( + description.GroupName.ToUpperInvariant(), + $"/openapi/{description.GroupName}/openapi.json")); + } + }); } + + return app; } } diff --git a/src/Nexus/Core/NexusOptions.cs b/src/Nexus/Core/NexusOptions.cs index 5b07b910..8b52aad1 100644 --- a/src/Nexus/Core/NexusOptions.cs +++ b/src/Nexus/Core/NexusOptions.cs @@ -1,105 +1,104 @@ using System.Runtime.InteropServices; -namespace Nexus.Core -{ - // TODO: Records with IConfiguration: wait for issue https://github.com/dotnet/runtime/issues/43662 to be solved +namespace Nexus.Core; - // template: https://grafana.com/docs/grafana/latest/administration/configuration/ +// TODO: Records with IConfiguration: wait for issue https://github.com/dotnet/runtime/issues/43662 to be solved - internal abstract record NexusOptionsBase() - { - // for testing only - public string? BlindSample { get; set; } +// template: https://grafana.com/docs/grafana/latest/administration/configuration/ - internal static IConfiguration BuildConfiguration(string[] args) - { - var environmentName = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"); +internal abstract record NexusOptionsBase() +{ + // for testing only + public string? BlindSample { get; set; } - var builder = new ConfigurationBuilder() - .AddJsonFile("appsettings.json"); + internal static IConfiguration BuildConfiguration(string[] args) + { + var environmentName = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"); - if (!string.IsNullOrWhiteSpace(environmentName)) - { - builder - .AddJsonFile($"appsettings.{environmentName}.json", optional: true, reloadOnChange: true); - } + var builder = new ConfigurationBuilder() + .AddJsonFile("appsettings.json"); - var settingsPath = Environment.GetEnvironmentVariable("NEXUS_PATHS__SETTINGS"); + if (!string.IsNullOrWhiteSpace(environmentName)) + { + builder + .AddJsonFile($"appsettings.{environmentName}.json", optional: true, reloadOnChange: true); + } - settingsPath ??= PathsOptions.DefaultSettingsPath; + var settingsPath = Environment.GetEnvironmentVariable("NEXUS_PATHS__SETTINGS"); - if (settingsPath.EndsWith(".json", StringComparison.OrdinalIgnoreCase)) - builder.AddJsonFile(settingsPath, optional: true, /* for serilog */ reloadOnChange: true); + settingsPath ??= PathsOptions.DefaultSettingsPath; - else if (settingsPath.EndsWith(".ini", StringComparison.OrdinalIgnoreCase)) - builder.AddIniFile(settingsPath, optional: true, /* for serilog */ reloadOnChange: true); + if (settingsPath.EndsWith(".json", StringComparison.OrdinalIgnoreCase)) + builder.AddJsonFile(settingsPath, optional: true, /* for serilog */ reloadOnChange: true); - builder - .AddEnvironmentVariables(prefix: "NEXUS_") - .AddCommandLine(args); + else if (settingsPath.EndsWith(".ini", StringComparison.OrdinalIgnoreCase)) + builder.AddIniFile(settingsPath, optional: true, /* for serilog */ reloadOnChange: true); - return builder.Build(); - } - } + builder + .AddEnvironmentVariables(prefix: "NEXUS_") + .AddCommandLine(args); - internal record GeneralOptions() : NexusOptionsBase - { - public const string Section = "General"; - public string? ApplicationName { get; set; } = "Nexus"; - public string? HelpLink { get; set; } - public string? DefaultFileType { get; set; } = "Nexus.Writers.Csv"; + return builder.Build(); } +} - internal record DataOptions() : NexusOptionsBase - { - public const string Section = "Data"; - public string? CachePattern { get; set; } - public long TotalBufferMemoryConsumption { get; set; } = 1 * 1024 * 1024 * 1024; // 1 GB - public double AggregationNaNThreshold { get; set; } = 0.99; - } +internal record GeneralOptions() : NexusOptionsBase +{ + public const string Section = "General"; + public string? ApplicationName { get; set; } = "Nexus"; + public string? HelpLink { get; set; } + public string? DefaultFileType { get; set; } = "Nexus.Writers.Csv"; +} - internal record PathsOptions() : NexusOptionsBase - { - public const string Section = "Paths"; +internal record DataOptions() : NexusOptionsBase +{ + public const string Section = "Data"; + public string? CachePattern { get; set; } + public long TotalBufferMemoryConsumption { get; set; } = 1 * 1024 * 1024 * 1024; // 1 GB + public double AggregationNaNThreshold { get; set; } = 0.99; +} - public string Config { get; set; } = Path.Combine(PlatformSpecificRoot, "config"); - public string Cache { get; set; } = Path.Combine(PlatformSpecificRoot, "cache"); - public string Catalogs { get; set; } = Path.Combine(PlatformSpecificRoot, "catalogs"); - public string Artifacts { get; set; } = Path.Combine(PlatformSpecificRoot, "artifacts"); - public string Users { get; set; } = Path.Combine(PlatformSpecificRoot, "users"); - public string Packages { get; set; } = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".nexus", "packages"); - // GetGlobalPackagesFolder: https://github.com/NuGet/NuGet.Client/blob/0fc58e13683565e7bdf30e706d49e58fc497bbed/src/NuGet.Core/NuGet.Configuration/Utility/SettingsUtility.cs#L225-L254 - // GetFolderPath: https://github.com/NuGet/NuGet.Client/blob/1d75910076b2ecfbe5f142227cfb4fb45c093a1e/src/NuGet.Core/NuGet.Common/PathUtil/NuGetEnvironment.cs#L54-L57 +internal record PathsOptions() : NexusOptionsBase +{ + public const string Section = "Paths"; - #region Support + public string Config { get; set; } = Path.Combine(PlatformSpecificRoot, "config"); + public string Cache { get; set; } = Path.Combine(PlatformSpecificRoot, "cache"); + public string Catalogs { get; set; } = Path.Combine(PlatformSpecificRoot, "catalogs"); + public string Artifacts { get; set; } = Path.Combine(PlatformSpecificRoot, "artifacts"); + public string Users { get; set; } = Path.Combine(PlatformSpecificRoot, "users"); + public string Packages { get; set; } = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".nexus", "packages"); + // GetGlobalPackagesFolder: https://github.com/NuGet/NuGet.Client/blob/0fc58e13683565e7bdf30e706d49e58fc497bbed/src/NuGet.Core/NuGet.Configuration/Utility/SettingsUtility.cs#L225-L254 + // GetFolderPath: https://github.com/NuGet/NuGet.Client/blob/1d75910076b2ecfbe5f142227cfb4fb45c093a1e/src/NuGet.Core/NuGet.Common/PathUtil/NuGetEnvironment.cs#L54-L57 - public static string DefaultSettingsPath { get; } = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) - ? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Nexus", "settings.json") - : "/etc/nexus/settings.json"; + #region Support - private static string PlatformSpecificRoot { get; } = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) - ? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Nexus") - : "/var/lib/nexus"; + public static string DefaultSettingsPath { get; } = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Nexus", "settings.json") + : "/etc/nexus/settings.json"; - #endregion - } + private static string PlatformSpecificRoot { get; } = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Nexus") + : "/var/lib/nexus"; - internal record OpenIdConnectProvider - { + #endregion +} + +internal record OpenIdConnectProvider +{ #pragma warning disable CS8618 // Ein Non-Nullable-Feld muss beim Beenden des Konstruktors einen Wert ungleich NULL enthalten. Erwägen Sie die Deklaration als Nullable. - public string Scheme { get; init; } - public string DisplayName { get; init; } - public string Authority { get; init; } - public string ClientId { get; init; } - public string ClientSecret { get; init; } + public string Scheme { get; init; } + public string DisplayName { get; init; } + public string Authority { get; init; } + public string ClientId { get; init; } + public string ClientSecret { get; init; } #pragma warning restore CS8618 // Ein Non-Nullable-Feld muss beim Beenden des Konstruktors einen Wert ungleich NULL enthalten. Erwägen Sie die Deklaration als Nullable. - } +} - internal partial record SecurityOptions() : NexusOptionsBase - { - public const string Section = "Security"; +internal partial record SecurityOptions() : NexusOptionsBase +{ + public const string Section = "Security"; - public TimeSpan CookieLifetime { get; set; } = TimeSpan.FromDays(30); - public List OidcProviders { get; set; } = new(); - } + public TimeSpan CookieLifetime { get; set; } = TimeSpan.FromDays(30); + public List OidcProviders { get; set; } = new(); } \ No newline at end of file diff --git a/src/Nexus/Core/NexusPolicies.cs b/src/Nexus/Core/NexusPolicies.cs index 263bc862..d17ee21e 100644 --- a/src/Nexus/Core/NexusPolicies.cs +++ b/src/Nexus/Core/NexusPolicies.cs @@ -1,7 +1,6 @@ -namespace Nexus.Core +namespace Nexus.Core; + +internal static class NexusPolicies { - internal static class NexusPolicies - { - public const string RequireAdmin = "RequireAdmin"; - } + public const string RequireAdmin = "RequireAdmin"; } diff --git a/src/Nexus/Core/NexusRoles.cs b/src/Nexus/Core/NexusRoles.cs index 2b931827..55beedae 100644 --- a/src/Nexus/Core/NexusRoles.cs +++ b/src/Nexus/Core/NexusRoles.cs @@ -1,8 +1,7 @@ -namespace Nexus.Core +namespace Nexus.Core; + +internal static class NexusRoles { - internal static class NexusRoles - { - public const string ADMINISTRATOR = "Administrator"; - public const string USER = "User"; - } + public const string ADMINISTRATOR = "Administrator"; + public const string USER = "User"; } diff --git a/src/Nexus/Core/StreamInputFormatter.cs b/src/Nexus/Core/StreamInputFormatter.cs index dd64349c..088ad916 100644 --- a/src/Nexus/Core/StreamInputFormatter.cs +++ b/src/Nexus/Core/StreamInputFormatter.cs @@ -1,17 +1,16 @@ using Microsoft.AspNetCore.Mvc.Formatters; -namespace Nexus.Core +namespace Nexus.Core; + +internal class StreamInputFormatter : IInputFormatter { - internal class StreamInputFormatter : IInputFormatter + public bool CanRead(InputFormatterContext context) { - public bool CanRead(InputFormatterContext context) - { - return context.HttpContext.Request.ContentType == "application/octet-stream"; - } + return context.HttpContext.Request.ContentType == "application/octet-stream"; + } - public async Task ReadAsync(InputFormatterContext context) - { - return await InputFormatterResult.SuccessAsync(context.HttpContext.Request.Body); - } + public async Task ReadAsync(InputFormatterContext context) + { + return await InputFormatterResult.SuccessAsync(context.HttpContext.Request.Body); } } diff --git a/src/Nexus/Core/UserDbContext.cs b/src/Nexus/Core/UserDbContext.cs index bfeb1cc7..0c8f1fd3 100644 --- a/src/Nexus/Core/UserDbContext.cs +++ b/src/Nexus/Core/UserDbContext.cs @@ -1,26 +1,25 @@ using Microsoft.EntityFrameworkCore; -namespace Nexus.Core +namespace Nexus.Core; + +internal class UserDbContext : DbContext { - internal class UserDbContext : DbContext + public UserDbContext(DbContextOptions options) + : base(options) { - public UserDbContext(DbContextOptions options) - : base(options) - { - // - } + // + } - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - modelBuilder - .Entity() - .HasOne(claim => claim.Owner) - .WithMany(user => user.Claims) - .IsRequired(); - } + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder + .Entity() + .HasOne(claim => claim.Owner) + .WithMany(user => user.Claims) + .IsRequired(); + } - public DbSet Users { get; set; } = default!; + public DbSet Users { get; set; } = default!; - public DbSet Claims { get; set; } = default!; - } + public DbSet Claims { get; set; } = default!; } diff --git a/src/Nexus/Extensibility/DataSource/DataSourceController.cs b/src/Nexus/Extensibility/DataSource/DataSourceController.cs index 68f961e8..c4c4c047 100644 --- a/src/Nexus/Extensibility/DataSource/DataSourceController.cs +++ b/src/Nexus/Extensibility/DataSource/DataSourceController.cs @@ -10,1103 +10,1102 @@ using Nexus.Services; using Nexus.Utilities; -namespace Nexus.Extensibility +namespace Nexus.Extensibility; + +internal interface IDataSourceController : IDisposable +{ + Task InitializeAsync( + ConcurrentDictionary catalogs, + ILogger logger, + CancellationToken cancellationToken); + + Task GetCatalogRegistrationsAsync( + string path, + CancellationToken cancellationToken); + + Task GetCatalogAsync( + string catalogId, + CancellationToken cancellationToken); + + Task GetAvailabilityAsync( + string catalogId, + DateTime begin, + DateTime end, + TimeSpan step, + CancellationToken cancellationToken); + + Task GetTimeRangeAsync( + string catalogId, + CancellationToken cancellationToken); + + Task IsDataOfDayAvailableAsync( + string catalogId, + DateTime day, + CancellationToken cancellationToken); + + Task ReadAsync( + DateTime begin, + DateTime end, + TimeSpan samplePeriod, + CatalogItemRequestPipeWriter[] catalogItemRequestPipeWriters, + ReadDataHandler readDataHandler, + IProgress progress, + CancellationToken cancellationToken); +} + +internal class DataSourceController : IDataSourceController { - internal interface IDataSourceController : IDisposable + #region Fields + + private readonly IProcessingService _processingService; + private readonly ICacheService _cacheService; + private readonly DataOptions _dataOptions; + private ConcurrentDictionary _catalogCache = default!; + + #endregion + + #region Constructors + + public DataSourceController( + IDataSource dataSource, + InternalDataSourceRegistration registration, + IReadOnlyDictionary? systemConfiguration, + IReadOnlyDictionary? requestConfiguration, + IProcessingService processingService, + ICacheService cacheService, + DataOptions dataOptions, + ILogger logger) { - Task InitializeAsync( - ConcurrentDictionary catalogs, - ILogger logger, - CancellationToken cancellationToken); - - Task GetCatalogRegistrationsAsync( - string path, - CancellationToken cancellationToken); - - Task GetCatalogAsync( - string catalogId, - CancellationToken cancellationToken); - - Task GetAvailabilityAsync( - string catalogId, - DateTime begin, - DateTime end, - TimeSpan step, - CancellationToken cancellationToken); - - Task GetTimeRangeAsync( - string catalogId, - CancellationToken cancellationToken); - - Task IsDataOfDayAvailableAsync( - string catalogId, - DateTime day, - CancellationToken cancellationToken); - - Task ReadAsync( - DateTime begin, - DateTime end, - TimeSpan samplePeriod, - CatalogItemRequestPipeWriter[] catalogItemRequestPipeWriters, - ReadDataHandler readDataHandler, - IProgress progress, - CancellationToken cancellationToken); + DataSource = dataSource; + DataSourceRegistration = registration; + SystemConfiguration = systemConfiguration; + RequestConfiguration = requestConfiguration; + Logger = logger; + + _processingService = processingService; + _cacheService = cacheService; + _dataOptions = dataOptions; } - internal class DataSourceController : IDataSourceController - { - #region Fields - - private readonly IProcessingService _processingService; - private readonly ICacheService _cacheService; - private readonly DataOptions _dataOptions; - private ConcurrentDictionary _catalogCache = default!; - - #endregion - - #region Constructors - - public DataSourceController( - IDataSource dataSource, - InternalDataSourceRegistration registration, - IReadOnlyDictionary? systemConfiguration, - IReadOnlyDictionary? requestConfiguration, - IProcessingService processingService, - ICacheService cacheService, - DataOptions dataOptions, - ILogger logger) - { - DataSource = dataSource; - DataSourceRegistration = registration; - SystemConfiguration = systemConfiguration; - RequestConfiguration = requestConfiguration; - Logger = logger; - - _processingService = processingService; - _cacheService = cacheService; - _dataOptions = dataOptions; - } + #endregion - #endregion + #region Properties - #region Properties + private IDataSource DataSource { get; } - private IDataSource DataSource { get; } + private InternalDataSourceRegistration DataSourceRegistration { get; } - private InternalDataSourceRegistration DataSourceRegistration { get; } + private IReadOnlyDictionary? SystemConfiguration { get; } - private IReadOnlyDictionary? SystemConfiguration { get; } + internal IReadOnlyDictionary? RequestConfiguration { get; } - internal IReadOnlyDictionary? RequestConfiguration { get; } + private ILogger Logger { get; } - private ILogger Logger { get; } + #endregion - #endregion + #region Methods - #region Methods + public async Task InitializeAsync( + ConcurrentDictionary catalogCache, + ILogger logger, + CancellationToken cancellationToken) + { + _catalogCache = catalogCache; - public async Task InitializeAsync( - ConcurrentDictionary catalogCache, - ILogger logger, - CancellationToken cancellationToken) - { - _catalogCache = catalogCache; + var clonedSourceConfiguration = DataSourceRegistration.Configuration is null + ? default + : DataSourceRegistration.Configuration.ToDictionary(entry => entry.Key, entry => entry.Value.Clone()); - var clonedSourceConfiguration = DataSourceRegistration.Configuration is null - ? default - : DataSourceRegistration.Configuration.ToDictionary(entry => entry.Key, entry => entry.Value.Clone()); + var context = new DataSourceContext( + ResourceLocator: DataSourceRegistration.ResourceLocator, + SystemConfiguration: SystemConfiguration, + SourceConfiguration: clonedSourceConfiguration, + RequestConfiguration: RequestConfiguration); - var context = new DataSourceContext( - ResourceLocator: DataSourceRegistration.ResourceLocator, - SystemConfiguration: SystemConfiguration, - SourceConfiguration: clonedSourceConfiguration, - RequestConfiguration: RequestConfiguration); + await DataSource.SetContextAsync(context, logger, cancellationToken); + } - await DataSource.SetContextAsync(context, logger, cancellationToken); - } + public async Task GetCatalogRegistrationsAsync( + string path, + CancellationToken cancellationToken) + { + var catalogRegistrations = await DataSource + .GetCatalogRegistrationsAsync(path, cancellationToken); - public async Task GetCatalogRegistrationsAsync( - string path, - CancellationToken cancellationToken) + for (int i = 0; i < catalogRegistrations.Length; i++) { - var catalogRegistrations = await DataSource - .GetCatalogRegistrationsAsync(path, cancellationToken); - - for (int i = 0; i < catalogRegistrations.Length; i++) + // absolute + if (catalogRegistrations[i].Path.StartsWith('/')) { - // absolute - if (catalogRegistrations[i].Path.StartsWith('/')) - { - if (!catalogRegistrations[i].Path.StartsWith(path)) - throw new Exception($"The catalog path {catalogRegistrations[i].Path} is not a sub path of {path}."); - } + if (!catalogRegistrations[i].Path.StartsWith(path)) + throw new Exception($"The catalog path {catalogRegistrations[i].Path} is not a sub path of {path}."); + } - // relative - else + // relative + else + { + catalogRegistrations[i] = catalogRegistrations[i] with { - catalogRegistrations[i] = catalogRegistrations[i] with - { - Path = path + catalogRegistrations[i].Path - }; - } + Path = path + catalogRegistrations[i].Path + }; } + } - if (catalogRegistrations.Any(catalogRegistration => !catalogRegistration.Path.StartsWith(path))) - throw new Exception($"The returned catalog identifier is not a child of {path}."); + if (catalogRegistrations.Any(catalogRegistration => !catalogRegistration.Path.StartsWith(path))) + throw new Exception($"The returned catalog identifier is not a child of {path}."); - return catalogRegistrations; - } + return catalogRegistrations; + } - public async Task GetCatalogAsync( - string catalogId, - CancellationToken cancellationToken) - { - Logger.LogDebug("Load catalog {CatalogId}", catalogId); + public async Task GetCatalogAsync( + string catalogId, + CancellationToken cancellationToken) + { + Logger.LogDebug("Load catalog {CatalogId}", catalogId); - var catalog = await DataSource.GetCatalogAsync(catalogId, cancellationToken); + var catalog = await DataSource.GetCatalogAsync(catalogId, cancellationToken); - if (catalog.Id != catalogId) - throw new Exception("The id of the returned catalog does not match the requested catalog id."); + if (catalog.Id != catalogId) + throw new Exception("The id of the returned catalog does not match the requested catalog id."); - catalog = catalog with - { - Resources = catalog.Resources?.OrderBy(resource => resource.Id).ToList() - }; + catalog = catalog with + { + Resources = catalog.Resources?.OrderBy(resource => resource.Id).ToList() + }; - // clean up "groups" property so it contains only unique groups - if (catalog.Resources is not null) + // clean up "groups" property so it contains only unique groups + if (catalog.Resources is not null) + { + var isModified = false; + var newResources = new List(); + + foreach (var resource in catalog.Resources) { - var isModified = false; - var newResources = new List(); + var resourceProperties = resource.Properties; + var groups = resourceProperties?.GetStringArray(DataModelExtensions.GroupsKey); + var newResource = resource; - foreach (var resource in catalog.Resources) + if (groups is not null) { - var resourceProperties = resource.Properties; - var groups = resourceProperties?.GetStringArray(DataModelExtensions.GroupsKey); - var newResource = resource; + var distinctGroups = groups + .Where(group => group is not null) + .Distinct(); - if (groups is not null) + if (!distinctGroups.SequenceEqual(groups)) { - var distinctGroups = groups - .Where(group => group is not null) - .Distinct(); + var jsonArray = new JsonArray(); - if (!distinctGroups.SequenceEqual(groups)) + foreach (var group in distinctGroups) { - var jsonArray = new JsonArray(); - - foreach (var group in distinctGroups) - { - jsonArray.Add(group); - } + jsonArray.Add(group); + } - var newResourceProperties = resourceProperties!.ToDictionary(entry => entry.Key, entry => entry.Value); - newResourceProperties[DataModelExtensions.GroupsKey] = JsonSerializer.SerializeToElement(jsonArray); + var newResourceProperties = resourceProperties!.ToDictionary(entry => entry.Key, entry => entry.Value); + newResourceProperties[DataModelExtensions.GroupsKey] = JsonSerializer.SerializeToElement(jsonArray); - newResource = resource with - { - Properties = newResourceProperties - }; + newResource = resource with + { + Properties = newResourceProperties + }; - isModified = true; - } + isModified = true; } - - newResources.Add(newResource); } - if (isModified) - { - catalog = catalog with - { - Resources = newResources - }; - } + newResources.Add(newResource); } - // TODO: Is it the best solution to inject these additional properties here? Similar code exists in SourcesController.GetExtensionDescriptions() - // add additional catalog properties - const string DATA_SOURCE_KEY = "data-source"; - var catalogProperties = catalog.Properties; - - if (catalogProperties is not null && - catalogProperties.TryGetValue(DATA_SOURCE_KEY, out var _)) + if (isModified) { - // do nothing + catalog = catalog with + { + Resources = newResources + }; } + } - else - { - var type = DataSource - .GetType(); + // TODO: Is it the best solution to inject these additional properties here? Similar code exists in SourcesController.GetExtensionDescriptions() + // add additional catalog properties + const string DATA_SOURCE_KEY = "data-source"; + var catalogProperties = catalog.Properties; - var nexusVersion = typeof(Program).Assembly - .GetCustomAttribute()! - .InformationalVersion; + if (catalogProperties is not null && + catalogProperties.TryGetValue(DATA_SOURCE_KEY, out var _)) + { + // do nothing + } - var dataSourceVersion = type.Assembly - .GetCustomAttribute()! - .InformationalVersion; + else + { + var type = DataSource + .GetType(); - var repositoryUrl = type - .GetCustomAttribute(inherit: false)! - .RepositoryUrl; + var nexusVersion = typeof(Program).Assembly + .GetCustomAttribute()! + .InformationalVersion; - var newResourceProperties = catalogProperties is null - ? new Dictionary() - : catalogProperties.ToDictionary(entry => entry.Key, entry => entry.Value); + var dataSourceVersion = type.Assembly + .GetCustomAttribute()! + .InformationalVersion; - var originJsonObject = new JsonObject() - { - ["origin"] = new JsonObject() - { - ["nexus-version"] = nexusVersion, - ["data-source-repository-url"] = repositoryUrl, - ["data-source-version"] = dataSourceVersion, - } - }; + var repositoryUrl = type + .GetCustomAttribute(inherit: false)! + .RepositoryUrl; - newResourceProperties[DATA_SOURCE_KEY] = JsonSerializer.SerializeToElement(originJsonObject); + var newResourceProperties = catalogProperties is null + ? new Dictionary() + : catalogProperties.ToDictionary(entry => entry.Key, entry => entry.Value); - catalog = catalog with + var originJsonObject = new JsonObject() + { + ["origin"] = new JsonObject() { - Properties = newResourceProperties - }; - } + ["nexus-version"] = nexusVersion, + ["data-source-repository-url"] = repositoryUrl, + ["data-source-version"] = dataSourceVersion, + } + }; - /* GetOrAdd is not working because it requires a synchronous delegate */ - _catalogCache.TryAdd(catalogId, catalog); + newResourceProperties[DATA_SOURCE_KEY] = JsonSerializer.SerializeToElement(originJsonObject); - return catalog; + catalog = catalog with + { + Properties = newResourceProperties + }; } - public async Task GetAvailabilityAsync( - string catalogId, - DateTime begin, - DateTime end, - TimeSpan step, - CancellationToken cancellationToken) - { - - var count = (int)Math.Ceiling((end - begin).Ticks / (double)step.Ticks); - var availabilities = new double[count]; + /* GetOrAdd is not working because it requires a synchronous delegate */ + _catalogCache.TryAdd(catalogId, catalog); - var tasks = new List(capacity: count); - var currentBegin = begin; - - for (int i = 0; i < count; i++) - { - var currentEnd = currentBegin + step; - var currentBegin_captured = currentBegin; - var i_captured = i; - - tasks.Add(Task.Run(async () => - { - var availability = await DataSource.GetAvailabilityAsync(catalogId, currentBegin_captured, currentEnd, cancellationToken); - availabilities[i_captured] = availability; - }, cancellationToken)); + return catalog; + } - currentBegin = currentEnd; - } + public async Task GetAvailabilityAsync( + string catalogId, + DateTime begin, + DateTime end, + TimeSpan step, + CancellationToken cancellationToken) + { - await Task.WhenAll(tasks); + var count = (int)Math.Ceiling((end - begin).Ticks / (double)step.Ticks); + var availabilities = new double[count]; - return new CatalogAvailability(Data: availabilities); - } + var tasks = new List(capacity: count); + var currentBegin = begin; - public async Task GetTimeRangeAsync( - string catalogId, - CancellationToken cancellationToken) + for (int i = 0; i < count; i++) { - (var begin, var end) = await DataSource.GetTimeRangeAsync(catalogId, cancellationToken); + var currentEnd = currentBegin + step; + var currentBegin_captured = currentBegin; + var i_captured = i; - return new CatalogTimeRange( - Begin: begin, - End: end); - } + tasks.Add(Task.Run(async () => + { + var availability = await DataSource.GetAvailabilityAsync(catalogId, currentBegin_captured, currentEnd, cancellationToken); + availabilities[i_captured] = availability; + }, cancellationToken)); - public async Task IsDataOfDayAvailableAsync( - string catalogId, - DateTime day, - CancellationToken cancellationToken) - { - return (await DataSource.GetAvailabilityAsync(catalogId, day, day.AddDays(1), cancellationToken)) > 0; + currentBegin = currentEnd; } - public async Task ReadAsync( - DateTime begin, - DateTime end, - TimeSpan samplePeriod, - CatalogItemRequestPipeWriter[] catalogItemRequestPipeWriters, - ReadDataHandler readDataHandler, - IProgress progress, - CancellationToken cancellationToken) - { - /* This method reads data from the data source or from the cache and optionally - * processes the data (aggregation, resampling). - * - * Normally, all data would be loaded at once using a single call to - * DataSource.ReadAsync(). But with caching involved, it is not uncommon - * to have only parts of the requested data available in cache. The rest needs to - * be loaded and processed as usual. This leads to fragmented read periods and thus - * often more than a single call to DataSource.ReadAsync() is necessary. - * - * However, during the first request the cache is filled and subsequent identical - * requests will from now on be served from the cache only. - */ - - /* preparation */ - var readUnits = PrepareReadUnits( - catalogItemRequestPipeWriters); + await Task.WhenAll(tasks); - var readingTasks = new List(capacity: readUnits.Length); - var targetElementCount = ExtensibilityUtilities.CalculateElementCount(begin, end, samplePeriod); - var targetByteCount = sizeof(double) * targetElementCount; + return new CatalogAvailability(Data: availabilities); + } - // TODO: access to totalProgress (see below) is not thread safe - var totalProgress = 0.0; + public async Task GetTimeRangeAsync( + string catalogId, + CancellationToken cancellationToken) + { + (var begin, var end) = await DataSource.GetTimeRangeAsync(catalogId, cancellationToken); - /* 'Original' branch - * - Read data into readUnit.ReadRequest (rented buffer) - * - Merge data / status and copy result into readUnit.DataWriter - */ - var originalReadUnits = readUnits - .Where(readUnit => readUnit.CatalogItemRequest.BaseItem is null) - .ToArray(); + return new CatalogTimeRange( + Begin: begin, + End: end); + } - Logger.LogTrace("Load {RepresentationCount} original representations", originalReadUnits.Length); + public async Task IsDataOfDayAvailableAsync( + string catalogId, + DateTime day, + CancellationToken cancellationToken) + { + return (await DataSource.GetAvailabilityAsync(catalogId, day, day.AddDays(1), cancellationToken)) > 0; + } - var originalProgress = new Progress(); - var originalProgressFactor = originalReadUnits.Length / (double)readUnits.Length; - var originalProgress_old = 0.0; + public async Task ReadAsync( + DateTime begin, + DateTime end, + TimeSpan samplePeriod, + CatalogItemRequestPipeWriter[] catalogItemRequestPipeWriters, + ReadDataHandler readDataHandler, + IProgress progress, + CancellationToken cancellationToken) + { + /* This method reads data from the data source or from the cache and optionally + * processes the data (aggregation, resampling). + * + * Normally, all data would be loaded at once using a single call to + * DataSource.ReadAsync(). But with caching involved, it is not uncommon + * to have only parts of the requested data available in cache. The rest needs to + * be loaded and processed as usual. This leads to fragmented read periods and thus + * often more than a single call to DataSource.ReadAsync() is necessary. + * + * However, during the first request the cache is filled and subsequent identical + * requests will from now on be served from the cache only. + */ + + /* preparation */ + var readUnits = PrepareReadUnits( + catalogItemRequestPipeWriters); + + var readingTasks = new List(capacity: readUnits.Length); + var targetElementCount = ExtensibilityUtilities.CalculateElementCount(begin, end, samplePeriod); + var targetByteCount = sizeof(double) * targetElementCount; + + // TODO: access to totalProgress (see below) is not thread safe + var totalProgress = 0.0; + + /* 'Original' branch + * - Read data into readUnit.ReadRequest (rented buffer) + * - Merge data / status and copy result into readUnit.DataWriter + */ + var originalReadUnits = readUnits + .Where(readUnit => readUnit.CatalogItemRequest.BaseItem is null) + .ToArray(); + + Logger.LogTrace("Load {RepresentationCount} original representations", originalReadUnits.Length); + + var originalProgress = new Progress(); + var originalProgressFactor = originalReadUnits.Length / (double)readUnits.Length; + var originalProgress_old = 0.0; + + originalProgress.ProgressChanged += (sender, progressValue) => + { + var actualProgress = progressValue - originalProgress_old; + originalProgress_old = progressValue; + totalProgress += actualProgress; + progress.Report(totalProgress); + }; + + var originalTask = ReadOriginalAsync( + begin, + end, + originalReadUnits, + readDataHandler, + targetElementCount, + targetByteCount, + originalProgress, + cancellationToken); + + readingTasks.Add(originalTask); + + /* 'Processing' branch + * - Read cached data into readUnit.DataWriter + * - Read remaining data into readUnit.ReadRequest + * - Process readUnit.ReadRequest data and copy result into readUnit.DataWriter + */ + var processingReadUnits = readUnits + .Where(readUnit => readUnit.CatalogItemRequest.BaseItem is not null) + .ToArray(); + + Logger.LogTrace("Load {RepresentationCount} processing representations", processingReadUnits.Length); + + var processingProgressFactor = 1 / (double)readUnits.Length; + + foreach (var processingReadUnit in processingReadUnits) + { + var processingProgress = new Progress(); + var processingProgress_old = 0.0; - originalProgress.ProgressChanged += (sender, progressValue) => + processingProgress.ProgressChanged += (sender, progressValue) => { - var actualProgress = progressValue - originalProgress_old; - originalProgress_old = progressValue; + var actualProgress = progressValue - processingProgress_old; + processingProgress_old = progressValue; totalProgress += actualProgress; progress.Report(totalProgress); }; - var originalTask = ReadOriginalAsync( - begin, - end, - originalReadUnits, - readDataHandler, - targetElementCount, - targetByteCount, - originalProgress, - cancellationToken); - - readingTasks.Add(originalTask); - - /* 'Processing' branch - * - Read cached data into readUnit.DataWriter - * - Read remaining data into readUnit.ReadRequest - * - Process readUnit.ReadRequest data and copy result into readUnit.DataWriter - */ - var processingReadUnits = readUnits - .Where(readUnit => readUnit.CatalogItemRequest.BaseItem is not null) - .ToArray(); + var kind = processingReadUnit.CatalogItemRequest.Item.Representation.Kind; - Logger.LogTrace("Load {RepresentationCount} processing representations", processingReadUnits.Length); + var processingTask = kind == RepresentationKind.Resampled - var processingProgressFactor = 1 / (double)readUnits.Length; + ? ReadResampledAsync( + begin, + end, + processingReadUnit, + readDataHandler, + targetByteCount, + processingProgress, + cancellationToken) - foreach (var processingReadUnit in processingReadUnits) - { - var processingProgress = new Progress(); - var processingProgress_old = 0.0; + : ReadAggregatedAsync( + begin, + end, + processingReadUnit, + readDataHandler, + targetByteCount, + processingProgress, + cancellationToken); - processingProgress.ProgressChanged += (sender, progressValue) => - { - var actualProgress = progressValue - processingProgress_old; - processingProgress_old = progressValue; - totalProgress += actualProgress; - progress.Report(totalProgress); - }; + readingTasks.Add(processingTask); + } - var kind = processingReadUnit.CatalogItemRequest.Item.Representation.Kind; - - var processingTask = kind == RepresentationKind.Resampled - - ? ReadResampledAsync( - begin, - end, - processingReadUnit, - readDataHandler, - targetByteCount, - processingProgress, - cancellationToken) - - : ReadAggregatedAsync( - begin, - end, - processingReadUnit, - readDataHandler, - targetByteCount, - processingProgress, - cancellationToken); - - readingTasks.Add(processingTask); - } + /* wait for tasks to finish */ + await NexusUtilities.WhenAllFailFastAsync(readingTasks, cancellationToken); + } - /* wait for tasks to finish */ - await NexusUtilities.WhenAllFailFastAsync(readingTasks, cancellationToken); - } + private async Task ReadOriginalAsync( + DateTime begin, + DateTime end, + ReadUnit[] originalUnits, + ReadDataHandler readDataHandler, + int targetElementCount, + int targetByteCount, + IProgress progress, + CancellationToken cancellationToken) + { + var tuples = originalUnits + .Select(readUnit => (readUnit, new ReadRequestManager(readUnit.CatalogItemRequest.Item, targetElementCount))) + .ToArray(); - private async Task ReadOriginalAsync( - DateTime begin, - DateTime end, - ReadUnit[] originalUnits, - ReadDataHandler readDataHandler, - int targetElementCount, - int targetByteCount, - IProgress progress, - CancellationToken cancellationToken) + try { - var tuples = originalUnits - .Select(readUnit => (readUnit, new ReadRequestManager(readUnit.CatalogItemRequest.Item, targetElementCount))) + var readRequests = tuples + .Select(manager => manager.Item2.Request) .ToArray(); try { - var readRequests = tuples - .Select(manager => manager.Item2.Request) - .ToArray(); + await DataSource.ReadAsync( + begin, + end, + readRequests, + readDataHandler, + progress, + cancellationToken); + } + catch (OutOfMemoryException) + { + throw; + } + catch (Exception ex) + { + Logger.LogError(ex, "Read original data period {Begin} to {End} failed", begin, end); + } - try - { - await DataSource.ReadAsync( - begin, - end, - readRequests, - readDataHandler, - progress, - cancellationToken); - } - catch (OutOfMemoryException) - { - throw; - } - catch (Exception ex) - { - Logger.LogError(ex, "Read original data period {Begin} to {End} failed", begin, end); - } + var readingTasks = new List(capacity: originalUnits.Length); - var readingTasks = new List(capacity: originalUnits.Length); + foreach (var (readUnit, readRequestManager) in tuples) + { + var (catalogItemRequest, dataWriter) = readUnit; + var (_, data, status) = readRequestManager.Request; - foreach (var (readUnit, readRequestManager) in tuples) + using var scope = Logger.BeginScope(new Dictionary() { - var (catalogItemRequest, dataWriter) = readUnit; - var (_, data, status) = readRequestManager.Request; + ["ResourcePath"] = catalogItemRequest.Item.ToPath() + }); - using var scope = Logger.BeginScope(new Dictionary() - { - ["ResourcePath"] = catalogItemRequest.Item.ToPath() - }); + cancellationToken.ThrowIfCancellationRequested(); - cancellationToken.ThrowIfCancellationRequested(); - - var buffer = dataWriter - .GetMemory(targetByteCount)[..targetByteCount]; - - var targetBuffer = new CastMemoryManager(buffer).Memory; + var buffer = dataWriter + .GetMemory(targetByteCount)[..targetByteCount]; - readingTasks.Add(Task.Run(async () => - { - BufferUtilities.ApplyRepresentationStatusByDataType( - catalogItemRequest.Item.Representation.DataType, - data, - status, - target: targetBuffer); - - /* update progress */ - Logger.LogTrace("Advance data pipe writer by {DataLength} bytes", targetByteCount); - dataWriter.Advance(targetByteCount); - await dataWriter.FlushAsync(); - }, cancellationToken)); - } + var targetBuffer = new CastMemoryManager(buffer).Memory; - /* wait for tasks to finish */ - await NexusUtilities.WhenAllFailFastAsync(readingTasks, cancellationToken); - } - finally - { - foreach (var (readUnit, readRequestManager) in tuples) + readingTasks.Add(Task.Run(async () => { - readRequestManager.Dispose(); - } + BufferUtilities.ApplyRepresentationStatusByDataType( + catalogItemRequest.Item.Representation.DataType, + data, + status, + target: targetBuffer); + + /* update progress */ + Logger.LogTrace("Advance data pipe writer by {DataLength} bytes", targetByteCount); + dataWriter.Advance(targetByteCount); + await dataWriter.FlushAsync(); + }, cancellationToken)); } - } - private async Task ReadAggregatedAsync( - DateTime begin, - DateTime end, - ReadUnit readUnit, - ReadDataHandler readDataHandler, - int targetByteCount, - IProgress progress, - CancellationToken cancellationToken) + /* wait for tasks to finish */ + await NexusUtilities.WhenAllFailFastAsync(readingTasks, cancellationToken); + } + finally { - var item = readUnit.CatalogItemRequest.Item; - var baseItem = readUnit.CatalogItemRequest.BaseItem!; - var samplePeriod = item.Representation.SamplePeriod; - var baseSamplePeriod = baseItem.Representation.SamplePeriod; - - /* target buffer */ - var buffer = readUnit.DataWriter - .GetMemory(targetByteCount)[..targetByteCount]; - - var targetBuffer = new CastMemoryManager(buffer).Memory; - - /* read request */ - var readElementCount = ExtensibilityUtilities.CalculateElementCount(begin, end, baseSamplePeriod); - - using var readRequestManager = new ReadRequestManager(baseItem, readElementCount); - var readRequest = readRequestManager.Request; - - /* go */ - try + foreach (var (readUnit, readRequestManager) in tuples) { - /* load data from cache */ - Logger.LogTrace("Load data from cache"); + readRequestManager.Dispose(); + } + } + } - List uncachedIntervals; + private async Task ReadAggregatedAsync( + DateTime begin, + DateTime end, + ReadUnit readUnit, + ReadDataHandler readDataHandler, + int targetByteCount, + IProgress progress, + CancellationToken cancellationToken) + { + var item = readUnit.CatalogItemRequest.Item; + var baseItem = readUnit.CatalogItemRequest.BaseItem!; + var samplePeriod = item.Representation.SamplePeriod; + var baseSamplePeriod = baseItem.Representation.SamplePeriod; - var disableCache = _dataOptions.CachePattern is not null && !Regex.IsMatch(readUnit.CatalogItemRequest.Item.Catalog.Id, _dataOptions.CachePattern); + /* target buffer */ + var buffer = readUnit.DataWriter + .GetMemory(targetByteCount)[..targetByteCount]; - if (disableCache) - { - uncachedIntervals = new List { new Interval(begin, end) }; - } + var targetBuffer = new CastMemoryManager(buffer).Memory; - else - { - uncachedIntervals = await _cacheService.ReadAsync( - item, - begin, - targetBuffer, - cancellationToken); - } + /* read request */ + var readElementCount = ExtensibilityUtilities.CalculateElementCount(begin, end, baseSamplePeriod); - /* load and process remaining data from source */ - Logger.LogTrace("Load and process {PeriodCount} uncached periods from source", uncachedIntervals.Count); + using var readRequestManager = new ReadRequestManager(baseItem, readElementCount); + var readRequest = readRequestManager.Request; - var elementSize = baseItem.Representation.ElementSize; - var sourceSamplePeriod = baseSamplePeriod; - var targetSamplePeriod = samplePeriod; + /* go */ + try + { + /* load data from cache */ + Logger.LogTrace("Load data from cache"); - var blockSize = item.Representation.Kind == RepresentationKind.Resampled - ? (int)(sourceSamplePeriod.Ticks / targetSamplePeriod.Ticks) - : (int)(targetSamplePeriod.Ticks / sourceSamplePeriod.Ticks); + List uncachedIntervals; - foreach (var interval in uncachedIntervals) - { - var offset = interval.Begin - begin; - var length = interval.End - interval.Begin; + var disableCache = _dataOptions.CachePattern is not null && !Regex.IsMatch(readUnit.CatalogItemRequest.Item.Catalog.Id, _dataOptions.CachePattern); - var slicedReadRequest = readRequest with - { - Data = readRequest.Data.Slice( - start: NexusUtilities.Scale(offset, sourceSamplePeriod) * elementSize, - length: NexusUtilities.Scale(length, sourceSamplePeriod) * elementSize), - - Status = readRequest.Status.Slice( - start: NexusUtilities.Scale(offset, sourceSamplePeriod), - length: NexusUtilities.Scale(length, sourceSamplePeriod)), - }; - - /* read */ - await DataSource.ReadAsync( - interval.Begin, - interval.End, - new[] { slicedReadRequest }, - readDataHandler, - progress, - cancellationToken); - - /* process */ - var slicedTargetBuffer = targetBuffer.Slice( - start: NexusUtilities.Scale(offset, targetSamplePeriod), - length: NexusUtilities.Scale(length, targetSamplePeriod)); - - _processingService.Aggregate( - baseItem.Representation.DataType, - item.Representation.Kind, - slicedReadRequest.Data, - slicedReadRequest.Status, - targetBuffer: slicedTargetBuffer, - blockSize); - } - - /* update cache */ - if (!disableCache) - { - await _cacheService.UpdateAsync( - item, - begin, - targetBuffer, - uncachedIntervals, - cancellationToken); - } - } - catch (OutOfMemoryException) - { - throw; - } - catch (Exception ex) + if (disableCache) { - Logger.LogError(ex, "Read aggregation data period {Begin} to {End} failed", begin, end); + uncachedIntervals = new List { new Interval(begin, end) }; } - finally + + else { - /* update progress */ - Logger.LogTrace("Advance data pipe writer by {DataLength} bytes", targetByteCount); - readUnit.DataWriter.Advance(targetByteCount); - await readUnit.DataWriter.FlushAsync(cancellationToken); + uncachedIntervals = await _cacheService.ReadAsync( + item, + begin, + targetBuffer, + cancellationToken); } - } - private async Task ReadResampledAsync( - DateTime begin, - DateTime end, - ReadUnit readUnit, - ReadDataHandler readDataHandler, - int targetByteCount, - IProgress progress, - CancellationToken cancellationToken) - { - var item = readUnit.CatalogItemRequest.Item; - var baseItem = readUnit.CatalogItemRequest.BaseItem!; - var samplePeriod = item.Representation.SamplePeriod; - var baseSamplePeriod = baseItem.Representation.SamplePeriod; - - /* target buffer */ - var buffer = readUnit.DataWriter - .GetMemory(targetByteCount)[..targetByteCount]; - - var targetBuffer = new CastMemoryManager(buffer).Memory; - - /* Calculate rounded begin and end values. - * - * Example: - * - * - sample period = 1 s - * - extract data from 00:00:00.200 to 00:00:01:700 @ sample period = 100 ms - * - * _ ___ <- roundedBegin - * | | - * | 1 s x <- offset: 200 ms - * | | - * |_ ___ <- roundedEnd - * | | - * | 1 s x <- end: length: 1500 ms - * | | - * |_ ___ - * - * roundedBegin = 00:00:00 - * roundedEnd = 00:00:02 - * offset = 200 ms == 2 elements - * length = 1500 ms == 15 elements - */ + /* load and process remaining data from source */ + Logger.LogTrace("Load and process {PeriodCount} uncached periods from source", uncachedIntervals.Count); - var roundedBegin = begin.RoundDown(baseSamplePeriod); - var roundedEnd = end.RoundUp(baseSamplePeriod); - var roundedElementCount = ExtensibilityUtilities.CalculateElementCount(roundedBegin, roundedEnd, baseSamplePeriod); + var elementSize = baseItem.Representation.ElementSize; + var sourceSamplePeriod = baseSamplePeriod; + var targetSamplePeriod = samplePeriod; - /* read request */ - using var readRequestManager = new ReadRequestManager(baseItem, roundedElementCount); - var readRequest = readRequestManager.Request; + var blockSize = item.Representation.Kind == RepresentationKind.Resampled + ? (int)(sourceSamplePeriod.Ticks / targetSamplePeriod.Ticks) + : (int)(targetSamplePeriod.Ticks / sourceSamplePeriod.Ticks); - /* go */ - try + foreach (var interval in uncachedIntervals) { - /* load and process data from source */ - var elementSize = baseItem.Representation.ElementSize; - var sourceSamplePeriod = baseSamplePeriod; - var targetSamplePeriod = samplePeriod; + var offset = interval.Begin - begin; + var length = interval.End - interval.Begin; - var blockSize = item.Representation.Kind == RepresentationKind.Resampled - ? (int)(sourceSamplePeriod.Ticks / targetSamplePeriod.Ticks) - : (int)(targetSamplePeriod.Ticks / sourceSamplePeriod.Ticks); + var slicedReadRequest = readRequest with + { + Data = readRequest.Data.Slice( + start: NexusUtilities.Scale(offset, sourceSamplePeriod) * elementSize, + length: NexusUtilities.Scale(length, sourceSamplePeriod) * elementSize), + + Status = readRequest.Status.Slice( + start: NexusUtilities.Scale(offset, sourceSamplePeriod), + length: NexusUtilities.Scale(length, sourceSamplePeriod)), + }; /* read */ await DataSource.ReadAsync( - roundedBegin, - roundedEnd, - new[] { readRequest }, + interval.Begin, + interval.End, + new[] { slicedReadRequest }, readDataHandler, progress, cancellationToken); /* process */ - var offset = NexusUtilities.Scale(begin - roundedBegin, targetSamplePeriod); + var slicedTargetBuffer = targetBuffer.Slice( + start: NexusUtilities.Scale(offset, targetSamplePeriod), + length: NexusUtilities.Scale(length, targetSamplePeriod)); - _processingService.Resample( + _processingService.Aggregate( baseItem.Representation.DataType, - readRequest.Data, - readRequest.Status, - targetBuffer, - blockSize, - offset); + item.Representation.Kind, + slicedReadRequest.Data, + slicedReadRequest.Status, + targetBuffer: slicedTargetBuffer, + blockSize); } - catch (OutOfMemoryException) - { - throw; - } - catch (Exception ex) + + /* update cache */ + if (!disableCache) { - Logger.LogError(ex, "Read resampling data period {Begin} to {End} failed", roundedBegin, roundedEnd); + await _cacheService.UpdateAsync( + item, + begin, + targetBuffer, + uncachedIntervals, + cancellationToken); } - + } + catch (OutOfMemoryException) + { + throw; + } + catch (Exception ex) + { + Logger.LogError(ex, "Read aggregation data period {Begin} to {End} failed", begin, end); + } + finally + { /* update progress */ Logger.LogTrace("Advance data pipe writer by {DataLength} bytes", targetByteCount); readUnit.DataWriter.Advance(targetByteCount); await readUnit.DataWriter.FlushAsync(cancellationToken); } + } - private ReadUnit[] PrepareReadUnits( - CatalogItemRequestPipeWriter[] catalogItemRequestPipeWriters) + private async Task ReadResampledAsync( + DateTime begin, + DateTime end, + ReadUnit readUnit, + ReadDataHandler readDataHandler, + int targetByteCount, + IProgress progress, + CancellationToken cancellationToken) + { + var item = readUnit.CatalogItemRequest.Item; + var baseItem = readUnit.CatalogItemRequest.BaseItem!; + var samplePeriod = item.Representation.SamplePeriod; + var baseSamplePeriod = baseItem.Representation.SamplePeriod; + + /* target buffer */ + var buffer = readUnit.DataWriter + .GetMemory(targetByteCount)[..targetByteCount]; + + var targetBuffer = new CastMemoryManager(buffer).Memory; + + /* Calculate rounded begin and end values. + * + * Example: + * + * - sample period = 1 s + * - extract data from 00:00:00.200 to 00:00:01:700 @ sample period = 100 ms + * + * _ ___ <- roundedBegin + * | | + * | 1 s x <- offset: 200 ms + * | | + * |_ ___ <- roundedEnd + * | | + * | 1 s x <- end: length: 1500 ms + * | | + * |_ ___ + * + * roundedBegin = 00:00:00 + * roundedEnd = 00:00:02 + * offset = 200 ms == 2 elements + * length = 1500 ms == 15 elements + */ + + var roundedBegin = begin.RoundDown(baseSamplePeriod); + var roundedEnd = end.RoundUp(baseSamplePeriod); + var roundedElementCount = ExtensibilityUtilities.CalculateElementCount(roundedBegin, roundedEnd, baseSamplePeriod); + + /* read request */ + using var readRequestManager = new ReadRequestManager(baseItem, roundedElementCount); + var readRequest = readRequestManager.Request; + + /* go */ + try { - var readUnits = new List(); + /* load and process data from source */ + var elementSize = baseItem.Representation.ElementSize; + var sourceSamplePeriod = baseSamplePeriod; + var targetSamplePeriod = samplePeriod; + + var blockSize = item.Representation.Kind == RepresentationKind.Resampled + ? (int)(sourceSamplePeriod.Ticks / targetSamplePeriod.Ticks) + : (int)(targetSamplePeriod.Ticks / sourceSamplePeriod.Ticks); + + /* read */ + await DataSource.ReadAsync( + roundedBegin, + roundedEnd, + new[] { readRequest }, + readDataHandler, + progress, + cancellationToken); - foreach (var catalogItemRequestPipeWriter in catalogItemRequestPipeWriters) - { - var (catalogItemRequest, dataWriter) = catalogItemRequestPipeWriter; + /* process */ + var offset = NexusUtilities.Scale(begin - roundedBegin, targetSamplePeriod); - var item = catalogItemRequest.BaseItem is null - ? catalogItemRequest.Item - : catalogItemRequest.BaseItem; + _processingService.Resample( + baseItem.Representation.DataType, + readRequest.Data, + readRequest.Status, + targetBuffer, + blockSize, + offset); + } + catch (OutOfMemoryException) + { + throw; + } + catch (Exception ex) + { + Logger.LogError(ex, "Read resampling data period {Begin} to {End} failed", roundedBegin, roundedEnd); + } - /* _catalogMap is guaranteed to contain the current catalog - * because GetCatalogAsync is called before ReadAsync */ - if (_catalogCache.TryGetValue(item.Catalog.Id, out var catalog)) - { - var readUnit = new ReadUnit(catalogItemRequest, dataWriter); - readUnits.Add(readUnit); - } + /* update progress */ + Logger.LogTrace("Advance data pipe writer by {DataLength} bytes", targetByteCount); + readUnit.DataWriter.Advance(targetByteCount); + await readUnit.DataWriter.FlushAsync(cancellationToken); + } - else - { - throw new Exception($"Cannot find catalog {item.Catalog.Id}."); - } + private ReadUnit[] PrepareReadUnits( + CatalogItemRequestPipeWriter[] catalogItemRequestPipeWriters) + { + var readUnits = new List(); + + foreach (var catalogItemRequestPipeWriter in catalogItemRequestPipeWriters) + { + var (catalogItemRequest, dataWriter) = catalogItemRequestPipeWriter; + + var item = catalogItemRequest.BaseItem is null + ? catalogItemRequest.Item + : catalogItemRequest.BaseItem; + + /* _catalogMap is guaranteed to contain the current catalog + * because GetCatalogAsync is called before ReadAsync */ + if (_catalogCache.TryGetValue(item.Catalog.Id, out var catalog)) + { + var readUnit = new ReadUnit(catalogItemRequest, dataWriter); + readUnits.Add(readUnit); } - return readUnits.ToArray(); + else + { + throw new Exception($"Cannot find catalog {item.Catalog.Id}."); + } } - #endregion + return readUnits.ToArray(); + } + + #endregion - #region Static Methods + #region Static Methods - public static async Task ReadAsync( - DateTime begin, - DateTime end, - TimeSpan samplePeriod, - DataReadingGroup[] readingGroups, - ReadDataHandler readDataHandler, - IMemoryTracker memoryTracker, - IProgress? progress, - ILogger logger, - CancellationToken cancellationToken) + public static async Task ReadAsync( + DateTime begin, + DateTime end, + TimeSpan samplePeriod, + DataReadingGroup[] readingGroups, + ReadDataHandler readDataHandler, + IMemoryTracker memoryTracker, + IProgress? progress, + ILogger logger, + CancellationToken cancellationToken) + { + /* validation */ + ValidateParameters(begin, end, samplePeriod); + + var catalogItemRequestPipeWriters = readingGroups.SelectMany(readingGroup => readingGroup.CatalogItemRequestPipeWriters); + + if (!catalogItemRequestPipeWriters.Any()) + return; + + foreach (var catalogItemRequestPipeWriter in catalogItemRequestPipeWriters) { - /* validation */ - ValidateParameters(begin, end, samplePeriod); + /* All frequencies are required to be multiples of each other, namely these are: + * + * - begin + * - end + * - item -> representation -> sample period + * - base item -> representation -> sample period + * + * This makes aggregation and caching much easier. + */ - var catalogItemRequestPipeWriters = readingGroups.SelectMany(readingGroup => readingGroup.CatalogItemRequestPipeWriters); + var request = catalogItemRequestPipeWriter.Request; + var itemSamplePeriod = request.Item.Representation.SamplePeriod; - if (!catalogItemRequestPipeWriters.Any()) - return; + if (itemSamplePeriod != samplePeriod) + throw new ValidationException("All representations must be based on the same sample period."); - foreach (var catalogItemRequestPipeWriter in catalogItemRequestPipeWriters) + if (request.BaseItem is not null) { - /* All frequencies are required to be multiples of each other, namely these are: - * - * - begin - * - end - * - item -> representation -> sample period - * - base item -> representation -> sample period - * - * This makes aggregation and caching much easier. - */ - - var request = catalogItemRequestPipeWriter.Request; - var itemSamplePeriod = request.Item.Representation.SamplePeriod; - - if (itemSamplePeriod != samplePeriod) - throw new ValidationException("All representations must be based on the same sample period."); + var baseItemSamplePeriod = request.BaseItem.Representation.SamplePeriod; - if (request.BaseItem is not null) + // resampling is only possible if base sample period < sample period + if (request.Item.Representation.Kind == RepresentationKind.Resampled) { - var baseItemSamplePeriod = request.BaseItem.Representation.SamplePeriod; - - // resampling is only possible if base sample period < sample period - if (request.Item.Representation.Kind == RepresentationKind.Resampled) - { - if (baseItemSamplePeriod < samplePeriod) - throw new ValidationException("Unable to resample data if the base sample period is <= the sample period."); + if (baseItemSamplePeriod < samplePeriod) + throw new ValidationException("Unable to resample data if the base sample period is <= the sample period."); - if (baseItemSamplePeriod.Ticks % itemSamplePeriod.Ticks != 0) - throw new ValidationException("For resampling, the base sample period must be a multiple of the sample period."); - } + if (baseItemSamplePeriod.Ticks % itemSamplePeriod.Ticks != 0) + throw new ValidationException("For resampling, the base sample period must be a multiple of the sample period."); + } - // aggregation is only possible if sample period > base sample period - else - { - if (samplePeriod < baseItemSamplePeriod) - throw new ValidationException("Unable to aggregate data if the sample period is <= the base sample period."); + // aggregation is only possible if sample period > base sample period + else + { + if (samplePeriod < baseItemSamplePeriod) + throw new ValidationException("Unable to aggregate data if the sample period is <= the base sample period."); - if (itemSamplePeriod.Ticks % baseItemSamplePeriod.Ticks != 0) - throw new ValidationException("For aggregation, the sample period must be a multiple of the base sample period."); - } + if (itemSamplePeriod.Ticks % baseItemSamplePeriod.Ticks != 0) + throw new ValidationException("For aggregation, the sample period must be a multiple of the base sample period."); } } + } - /* total period */ - var totalPeriod = end - begin; - logger.LogTrace("The total period is {TotalPeriod}", totalPeriod); - - /* bytes per row */ + /* total period */ + var totalPeriod = end - begin; + logger.LogTrace("The total period is {TotalPeriod}", totalPeriod); - // If the user requests /xxx/10_min_mean#base=10_ms, then the algorithm below will assume a period - // of 10 minutes and a sample period of 10 ms, which leads to an estimated row size of 8 * 60000 = 480000 bytes. - // The algorithm works this way because it cannot know if the data are already cached. It also does not know - // if the data source will request more data which further increases the memory consumption. + /* bytes per row */ - var bytesPerRow = 0L; - var largestSamplePeriod = samplePeriod; + // If the user requests /xxx/10_min_mean#base=10_ms, then the algorithm below will assume a period + // of 10 minutes and a sample period of 10 ms, which leads to an estimated row size of 8 * 60000 = 480000 bytes. + // The algorithm works this way because it cannot know if the data are already cached. It also does not know + // if the data source will request more data which further increases the memory consumption. - foreach (var catalogItemRequestPipeWriter in catalogItemRequestPipeWriters) - { - var request = catalogItemRequestPipeWriter.Request; + var bytesPerRow = 0L; + var largestSamplePeriod = samplePeriod; - var elementSize = request.Item.Representation.ElementSize; - var elementCount = 1L; + foreach (var catalogItemRequestPipeWriter in catalogItemRequestPipeWriters) + { + var request = catalogItemRequestPipeWriter.Request; - if (request.BaseItem is not null) - { - var baseItemSamplePeriod = request.BaseItem.Representation.SamplePeriod; - var itemSamplePeriod = request.Item.Representation.SamplePeriod; + var elementSize = request.Item.Representation.ElementSize; + var elementCount = 1L; - if (request.Item.Representation.Kind == RepresentationKind.Resampled) - { - if (largestSamplePeriod < baseItemSamplePeriod) - largestSamplePeriod = baseItemSamplePeriod; - } + if (request.BaseItem is not null) + { + var baseItemSamplePeriod = request.BaseItem.Representation.SamplePeriod; + var itemSamplePeriod = request.Item.Representation.SamplePeriod; - else - { - elementCount = - itemSamplePeriod.Ticks / - baseItemSamplePeriod.Ticks; - } + if (request.Item.Representation.Kind == RepresentationKind.Resampled) + { + if (largestSamplePeriod < baseItemSamplePeriod) + largestSamplePeriod = baseItemSamplePeriod; } - bytesPerRow += Math.Max(1, elementCount) * elementSize; + else + { + elementCount = + itemSamplePeriod.Ticks / + baseItemSamplePeriod.Ticks; + } } - logger.LogTrace("A single row has a size of {BytesPerRow} bytes", bytesPerRow); + bytesPerRow += Math.Max(1, elementCount) * elementSize; + } - /* total memory consumption */ - var totalRowCount = totalPeriod.Ticks / samplePeriod.Ticks; - var totalByteCount = totalRowCount * bytesPerRow; + logger.LogTrace("A single row has a size of {BytesPerRow} bytes", bytesPerRow); - /* actual memory consumption / chunk size */ - var allocationRegistration = await memoryTracker.RegisterAllocationAsync( - minimumByteCount: bytesPerRow, maximumByteCount: totalByteCount, cancellationToken); + /* total memory consumption */ + var totalRowCount = totalPeriod.Ticks / samplePeriod.Ticks; + var totalByteCount = totalRowCount * bytesPerRow; - /* go */ - var chunkSize = allocationRegistration.ActualByteCount; - logger.LogTrace("The chunk size is {ChunkSize} bytes", chunkSize); + /* actual memory consumption / chunk size */ + var allocationRegistration = await memoryTracker.RegisterAllocationAsync( + minimumByteCount: bytesPerRow, maximumByteCount: totalByteCount, cancellationToken); - var rowCount = chunkSize / bytesPerRow; - logger.LogTrace("{RowCount} rows can be processed per chunk", rowCount); + /* go */ + var chunkSize = allocationRegistration.ActualByteCount; + logger.LogTrace("The chunk size is {ChunkSize} bytes", chunkSize); - var maxPeriodPerRequest = TimeSpan - .FromTicks(samplePeriod.Ticks * rowCount) - .RoundDown(largestSamplePeriod); + var rowCount = chunkSize / bytesPerRow; + logger.LogTrace("{RowCount} rows can be processed per chunk", rowCount); - if (maxPeriodPerRequest == TimeSpan.Zero) - throw new ValidationException("Unable to load the requested data because the available chunk size is too low."); + var maxPeriodPerRequest = TimeSpan + .FromTicks(samplePeriod.Ticks * rowCount) + .RoundDown(largestSamplePeriod); - logger.LogTrace("The maximum period per request is {MaxPeriodPerRequest}", maxPeriodPerRequest); + if (maxPeriodPerRequest == TimeSpan.Zero) + throw new ValidationException("Unable to load the requested data because the available chunk size is too low."); - try - { - await ReadCoreAsync( - begin, - totalPeriod, - maxPeriodPerRequest, - samplePeriod, - readingGroups, - readDataHandler, - progress, - logger, - cancellationToken); - } - finally - { - allocationRegistration.Dispose(); - } - } + logger.LogTrace("The maximum period per request is {MaxPeriodPerRequest}", maxPeriodPerRequest); - private static Task ReadCoreAsync( - DateTime begin, - TimeSpan totalPeriod, - TimeSpan maxPeriodPerRequest, - TimeSpan samplePeriod, - DataReadingGroup[] readingGroups, - ReadDataHandler readDataHandler, - IProgress? progress, - ILogger logger, - CancellationToken cancellationToken - ) + try + { + await ReadCoreAsync( + begin, + totalPeriod, + maxPeriodPerRequest, + samplePeriod, + readingGroups, + readDataHandler, + progress, + logger, + cancellationToken); + } + finally { - /* periods */ - var consumedPeriod = TimeSpan.Zero; - var remainingPeriod = totalPeriod; - var currentPeriod = default(TimeSpan); + allocationRegistration.Dispose(); + } + } - /* progress */ - var currentDataSourceProgress = new ConcurrentDictionary(); + private static Task ReadCoreAsync( + DateTime begin, + TimeSpan totalPeriod, + TimeSpan maxPeriodPerRequest, + TimeSpan samplePeriod, + DataReadingGroup[] readingGroups, + ReadDataHandler readDataHandler, + IProgress? progress, + ILogger logger, + CancellationToken cancellationToken + ) + { + /* periods */ + var consumedPeriod = TimeSpan.Zero; + var remainingPeriod = totalPeriod; + var currentPeriod = default(TimeSpan); - return Task.Run(async () => + /* progress */ + var currentDataSourceProgress = new ConcurrentDictionary(); + + return Task.Run(async () => + { + while (consumedPeriod < totalPeriod) { - while (consumedPeriod < totalPeriod) - { - cancellationToken.ThrowIfCancellationRequested(); + cancellationToken.ThrowIfCancellationRequested(); - currentDataSourceProgress.Clear(); - currentPeriod = TimeSpan.FromTicks(Math.Min(remainingPeriod.Ticks, maxPeriodPerRequest.Ticks)); + currentDataSourceProgress.Clear(); + currentPeriod = TimeSpan.FromTicks(Math.Min(remainingPeriod.Ticks, maxPeriodPerRequest.Ticks)); - var currentBegin = begin + consumedPeriod; - var currentEnd = currentBegin + currentPeriod; + var currentBegin = begin + consumedPeriod; + var currentEnd = currentBegin + currentPeriod; - logger.LogTrace("Process period {CurrentBegin} to {CurrentEnd}", currentBegin, currentEnd); + logger.LogTrace("Process period {CurrentBegin} to {CurrentEnd}", currentBegin, currentEnd); - var readingTasks = readingGroups.Select(async readingGroup => + var readingTasks = readingGroups.Select(async readingGroup => + { + var (controller, catalogItemRequestPipeWriters) = readingGroup; + + try { - var (controller, catalogItemRequestPipeWriters) = readingGroup; + /* no need to remove handler because of short lifetime of IDataSource */ + var dataSourceProgress = new Progress(); - try + dataSourceProgress.ProgressChanged += (sender, progressValue) => { - /* no need to remove handler because of short lifetime of IDataSource */ - var dataSourceProgress = new Progress(); - - dataSourceProgress.ProgressChanged += (sender, progressValue) => + if (progressValue <= 1) { - if (progressValue <= 1) - { - // https://stackoverflow.com/a/62768272 (currentDataSourceProgress) - currentDataSourceProgress.AddOrUpdate(controller, progressValue, (_, _) => progressValue); - - var baseProgress = consumedPeriod.Ticks / (double)totalPeriod.Ticks; - var relativeProgressFactor = currentPeriod.Ticks / (double)totalPeriod.Ticks; - var relativeProgress = currentDataSourceProgress.Sum(entry => entry.Value) * relativeProgressFactor; - - progress?.Report(baseProgress + relativeProgress); - } - }; - - await controller.ReadAsync( - currentBegin, - currentEnd, - samplePeriod, - catalogItemRequestPipeWriters, - readDataHandler, - dataSourceProgress, - cancellationToken); - } - catch (OutOfMemoryException) - { - throw; - } - catch (Exception ex) - { - logger.LogError(ex, "Process period {Begin} to {End} failed", currentBegin, currentEnd); - } - }).ToList(); - - await NexusUtilities.WhenAllFailFastAsync(readingTasks, cancellationToken); + // https://stackoverflow.com/a/62768272 (currentDataSourceProgress) + currentDataSourceProgress.AddOrUpdate(controller, progressValue, (_, _) => progressValue); - /* continue in time */ - consumedPeriod += currentPeriod; - remainingPeriod -= currentPeriod; + var baseProgress = consumedPeriod.Ticks / (double)totalPeriod.Ticks; + var relativeProgressFactor = currentPeriod.Ticks / (double)totalPeriod.Ticks; + var relativeProgress = currentDataSourceProgress.Sum(entry => entry.Value) * relativeProgressFactor; - progress?.Report(consumedPeriod.Ticks / (double)totalPeriod.Ticks); - } - - /* complete */ - foreach (var readingGroup in readingGroups) - { - foreach (var catalogItemRequestPipeWriter in readingGroup.CatalogItemRequestPipeWriters) + progress?.Report(baseProgress + relativeProgress); + } + }; + + await controller.ReadAsync( + currentBegin, + currentEnd, + samplePeriod, + catalogItemRequestPipeWriters, + readDataHandler, + dataSourceProgress, + cancellationToken); + } + catch (OutOfMemoryException) { - cancellationToken.ThrowIfCancellationRequested(); - - await catalogItemRequestPipeWriter.DataWriter.CompleteAsync(); + throw; } - } - }, cancellationToken); - } + catch (Exception ex) + { + logger.LogError(ex, "Process period {Begin} to {End} failed", currentBegin, currentEnd); + } + }).ToList(); - private static void ValidateParameters( - DateTime begin, - DateTime end, - TimeSpan samplePeriod) - { - /* When the user requests two time series of the same frequency, they will be aligned to the sample - * period. With the current implementation, it simply not possible for one data source to provide an - * offset which is smaller than the sample period. In future a solution could be to have time series - * data with associated time stamps, which is not yet implemented. - */ + await NexusUtilities.WhenAllFailFastAsync(readingTasks, cancellationToken); - /* Examples - * - * OK: from 2020-01-01 00:00:01.000 to 2020-01-01 00:00:03.000 @ 1 s - * - * FAIL: from 2020-01-01 00:00:00.000 to 2020-01-02 00:00:00.000 @ 130 ms - * OK: from 2020-01-01 00:00:00.050 to 2020-01-02 00:00:00.000 @ 130 ms - * - */ + /* continue in time */ + consumedPeriod += currentPeriod; + remainingPeriod -= currentPeriod; + progress?.Report(consumedPeriod.Ticks / (double)totalPeriod.Ticks); + } - if (begin >= end) - throw new ValidationException("The begin datetime must be less than the end datetime."); + /* complete */ + foreach (var readingGroup in readingGroups) + { + foreach (var catalogItemRequestPipeWriter in readingGroup.CatalogItemRequestPipeWriters) + { + cancellationToken.ThrowIfCancellationRequested(); - if (begin.Ticks % samplePeriod.Ticks != 0) - throw new ValidationException("The begin parameter must be a multiple of the sample period."); + await catalogItemRequestPipeWriter.DataWriter.CompleteAsync(); + } + } + }, cancellationToken); + } - if (end.Ticks % samplePeriod.Ticks != 0) - throw new ValidationException("The end parameter must be a multiple of the sample period."); - } + private static void ValidateParameters( + DateTime begin, + DateTime end, + TimeSpan samplePeriod) + { + /* When the user requests two time series of the same frequency, they will be aligned to the sample + * period. With the current implementation, it simply not possible for one data source to provide an + * offset which is smaller than the sample period. In future a solution could be to have time series + * data with associated time stamps, which is not yet implemented. + */ + + /* Examples + * + * OK: from 2020-01-01 00:00:01.000 to 2020-01-01 00:00:03.000 @ 1 s + * + * FAIL: from 2020-01-01 00:00:00.000 to 2020-01-02 00:00:00.000 @ 130 ms + * OK: from 2020-01-01 00:00:00.050 to 2020-01-02 00:00:00.000 @ 130 ms + * + */ + + + if (begin >= end) + throw new ValidationException("The begin datetime must be less than the end datetime."); + + if (begin.Ticks % samplePeriod.Ticks != 0) + throw new ValidationException("The begin parameter must be a multiple of the sample period."); + + if (end.Ticks % samplePeriod.Ticks != 0) + throw new ValidationException("The end parameter must be a multiple of the sample period."); + } - #endregion + #endregion - #region IDisposable + #region IDisposable - private bool _disposedValue; + private bool _disposedValue; - protected virtual void Dispose(bool disposing) + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) { - if (!_disposedValue) + if (disposing) { - if (disposing) - { - var disposable = DataSource as IDisposable; - disposable?.Dispose(); - } - - _disposedValue = true; + var disposable = DataSource as IDisposable; + disposable?.Dispose(); } - } - public void Dispose() - { - Dispose(disposing: true); - GC.SuppressFinalize(this); + _disposedValue = true; } + } - #endregion + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); } + + #endregion } diff --git a/src/Nexus/Extensibility/DataSource/DataSourceControllerExtensions.cs b/src/Nexus/Extensibility/DataSource/DataSourceControllerExtensions.cs index dfc0fdc7..5c42cb69 100644 --- a/src/Nexus/Extensibility/DataSource/DataSourceControllerExtensions.cs +++ b/src/Nexus/Extensibility/DataSource/DataSourceControllerExtensions.cs @@ -4,92 +4,91 @@ using Nexus.Utilities; using System.IO.Pipelines; -namespace Nexus.Extensibility +namespace Nexus.Extensibility; + +internal static class DataSourceControllerExtensions { - internal static class DataSourceControllerExtensions + public static DataSourceDoubleStream ReadAsStream( + this IDataSourceController controller, + DateTime begin, + DateTime end, + CatalogItemRequest request, + ReadDataHandler readDataHandler, + IMemoryTracker memoryTracker, + ILogger logger, + CancellationToken cancellationToken) { - public static DataSourceDoubleStream ReadAsStream( - this IDataSourceController controller, - DateTime begin, - DateTime end, - CatalogItemRequest request, - ReadDataHandler readDataHandler, - IMemoryTracker memoryTracker, - ILogger logger, - CancellationToken cancellationToken) - { - // DataSourceDoubleStream is only required to enable the browser to determine the download progress. - // Otherwise the PipeReader.AsStream() would be sufficient. + // DataSourceDoubleStream is only required to enable the browser to determine the download progress. + // Otherwise the PipeReader.AsStream() would be sufficient. - var samplePeriod = request.Item.Representation.SamplePeriod; - var elementCount = ExtensibilityUtilities.CalculateElementCount(begin, end, samplePeriod); - var totalLength = elementCount * NexusUtilities.SizeOf(NexusDataType.FLOAT64); - var pipe = new Pipe(); - var stream = new DataSourceDoubleStream(totalLength, pipe.Reader); + var samplePeriod = request.Item.Representation.SamplePeriod; + var elementCount = ExtensibilityUtilities.CalculateElementCount(begin, end, samplePeriod); + var totalLength = elementCount * NexusUtilities.SizeOf(NexusDataType.FLOAT64); + var pipe = new Pipe(); + var stream = new DataSourceDoubleStream(totalLength, pipe.Reader); - var task = controller.ReadSingleAsync( - begin, - end, - request, - pipe.Writer, - readDataHandler, - memoryTracker, - progress: default, - logger, - cancellationToken); + var task = controller.ReadSingleAsync( + begin, + end, + request, + pipe.Writer, + readDataHandler, + memoryTracker, + progress: default, + logger, + cancellationToken); - _ = Task.Run(async () => + _ = Task.Run(async () => + { + try { - try - { #pragma warning disable VSTHRD003 // Vermeiden Sie das Warten auf fremde Aufgaben - await task; + await task; #pragma warning restore VSTHRD003 // Vermeiden Sie das Warten auf fremde Aufgaben - } - catch (Exception ex) - { - logger.LogError(ex, "Streaming failed"); - stream.Cancel(); - } - finally - { - // here is the only logical place to dispose the controller - controller.Dispose(); - } - }, cancellationToken); + } + catch (Exception ex) + { + logger.LogError(ex, "Streaming failed"); + stream.Cancel(); + } + finally + { + // here is the only logical place to dispose the controller + controller.Dispose(); + } + }, cancellationToken); - return stream; - } + return stream; + } - public static Task ReadSingleAsync( - this IDataSourceController controller, - DateTime begin, - DateTime end, - CatalogItemRequest request, - PipeWriter dataWriter, - ReadDataHandler readDataHandler, - IMemoryTracker memoryTracker, - IProgress? progress, - ILogger logger, - CancellationToken cancellationToken) - { - var samplePeriod = request.Item.Representation.SamplePeriod; + public static Task ReadSingleAsync( + this IDataSourceController controller, + DateTime begin, + DateTime end, + CatalogItemRequest request, + PipeWriter dataWriter, + ReadDataHandler readDataHandler, + IMemoryTracker memoryTracker, + IProgress? progress, + ILogger logger, + CancellationToken cancellationToken) + { + var samplePeriod = request.Item.Representation.SamplePeriod; - var readingGroup = new DataReadingGroup(controller, new CatalogItemRequestPipeWriter[] - { - new CatalogItemRequestPipeWriter(request, dataWriter) - }); + var readingGroup = new DataReadingGroup(controller, new CatalogItemRequestPipeWriter[] + { + new CatalogItemRequestPipeWriter(request, dataWriter) + }); - return DataSourceController.ReadAsync( - begin, - end, - samplePeriod, - new DataReadingGroup[] { readingGroup }, - readDataHandler, - memoryTracker, - progress, - logger, - cancellationToken); - } + return DataSourceController.ReadAsync( + begin, + end, + samplePeriod, + new DataReadingGroup[] { readingGroup }, + readDataHandler, + memoryTracker, + progress, + logger, + cancellationToken); } } \ No newline at end of file diff --git a/src/Nexus/Extensibility/DataSource/DataSourceControllerTypes.cs b/src/Nexus/Extensibility/DataSource/DataSourceControllerTypes.cs index 3adbe583..c74ff03e 100644 --- a/src/Nexus/Extensibility/DataSource/DataSourceControllerTypes.cs +++ b/src/Nexus/Extensibility/DataSource/DataSourceControllerTypes.cs @@ -1,13 +1,12 @@ using Nexus.Core; using System.IO.Pipelines; -namespace Nexus.Extensibility -{ - internal record CatalogItemRequestPipeWriter( - CatalogItemRequest Request, - PipeWriter DataWriter); +namespace Nexus.Extensibility; - internal record DataReadingGroup( - IDataSourceController Controller, - CatalogItemRequestPipeWriter[] CatalogItemRequestPipeWriters); -} +internal record CatalogItemRequestPipeWriter( + CatalogItemRequest Request, + PipeWriter DataWriter); + +internal record DataReadingGroup( + IDataSourceController Controller, + CatalogItemRequestPipeWriter[] CatalogItemRequestPipeWriters); diff --git a/src/Nexus/Extensibility/DataSource/DataSourceDoubleStream.cs b/src/Nexus/Extensibility/DataSource/DataSourceDoubleStream.cs index 2a6dad88..f287b1b8 100644 --- a/src/Nexus/Extensibility/DataSource/DataSourceDoubleStream.cs +++ b/src/Nexus/Extensibility/DataSource/DataSourceDoubleStream.cs @@ -1,124 +1,123 @@ using System.IO.Pipelines; -namespace Nexus.Extensibility -{ - internal class DataSourceDoubleStream : Stream - { - #region Fields - - private readonly CancellationTokenSource _cts = new(); - private long _position; - private readonly long _length; - private readonly PipeReader _reader; - private readonly Stream _stream; +namespace Nexus.Extensibility; - #endregion +internal class DataSourceDoubleStream : Stream +{ + #region Fields - #region Constructors + private readonly CancellationTokenSource _cts = new(); + private long _position; + private readonly long _length; + private readonly PipeReader _reader; + private readonly Stream _stream; - public DataSourceDoubleStream(long length, PipeReader reader) - { - _length = length; - _reader = reader; - _stream = reader.AsStream(); - } + #endregion - #endregion + #region Constructors - #region Properties + public DataSourceDoubleStream(long length, PipeReader reader) + { + _length = length; + _reader = reader; + _stream = reader.AsStream(); + } - public override bool CanRead => true; + #endregion - public override bool CanSeek => false; + #region Properties - public override bool CanWrite => false; + public override bool CanRead => true; - public override long Length => _length; + public override bool CanSeek => false; - public override long Position - { - get - { - return _position; - } - set - { - throw new NotImplementedException(); - } - } + public override bool CanWrite => false; - #endregion + public override long Length => _length; - #region Methods - - public void Cancel() + public override long Position + { + get { - _reader.CancelPendingRead(); + return _position; } - - public override void Flush() + set { throw new NotImplementedException(); } + } - public override int Read(byte[] buffer, int offset, int count) - { - throw new NotImplementedException(); - } + #endregion - public override int Read(Span buffer) - { - throw new NotImplementedException(); - } + #region Methods - public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) - { - throw new NotImplementedException(); - } + public void Cancel() + { + _reader.CancelPendingRead(); + } - public override int EndRead(IAsyncResult asyncResult) - { - throw new NotImplementedException(); - } + public override void Flush() + { + throw new NotImplementedException(); + } - public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } + public override int Read(byte[] buffer, int offset, int count) + { + throw new NotImplementedException(); + } - public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) - { - var readCount = await _stream.ReadAsync(buffer, _cts.Token); - _position += readCount; + public override int Read(Span buffer) + { + throw new NotImplementedException(); + } - return readCount; - } + public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) + { + throw new NotImplementedException(); + } - public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } + public override int EndRead(IAsyncResult asyncResult) + { + throw new NotImplementedException(); + } - public override long Seek(long offset, SeekOrigin origin) - { - throw new NotImplementedException(); - } + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } - public override void SetLength(long value) - { - throw new NotImplementedException(); - } + public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + var readCount = await _stream.ReadAsync(buffer, _cts.Token); + _position += readCount; - public override void Write(byte[] buffer, int offset, int count) - { - throw new NotImplementedException(); - } + return readCount; + } - protected override void Dispose(bool disposing) - { - _stream.Dispose(); - } + public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotImplementedException(); + } - #endregion + public override void SetLength(long value) + { + throw new NotImplementedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotImplementedException(); } + + protected override void Dispose(bool disposing) + { + _stream.Dispose(); + } + + #endregion } diff --git a/src/Nexus/Extensibility/DataWriter/DataWriterController.cs b/src/Nexus/Extensibility/DataWriter/DataWriterController.cs index d0620559..f516b551 100644 --- a/src/Nexus/Extensibility/DataWriter/DataWriterController.cs +++ b/src/Nexus/Extensibility/DataWriter/DataWriterController.cs @@ -4,276 +4,275 @@ using System.ComponentModel.DataAnnotations; using System.Text.Json; -namespace Nexus.Extensibility +namespace Nexus.Extensibility; + +internal interface IDataWriterController : IDisposable +{ + Task InitializeAsync( + ILogger logger, + CancellationToken cancellationToken); + + Task WriteAsync( + DateTime begin, + DateTime end, + TimeSpan samplePeriod, + TimeSpan filePeriod, + CatalogItemRequestPipeReader[] catalogItemRequestPipeReaders, + IProgress progress, + CancellationToken cancellationToken); +} + +// TODO: Add "CheckFileSize" method (e.g. for Famos). + +internal class DataWriterController : IDataWriterController { - internal interface IDataWriterController : IDisposable + #region Constructors + + public DataWriterController( + IDataWriter dataWriter, + Uri resourceLocator, + IReadOnlyDictionary? systemConfiguration, + IReadOnlyDictionary? requestConfiguration, + ILogger logger) { - Task InitializeAsync( - ILogger logger, - CancellationToken cancellationToken); - - Task WriteAsync( - DateTime begin, - DateTime end, - TimeSpan samplePeriod, - TimeSpan filePeriod, - CatalogItemRequestPipeReader[] catalogItemRequestPipeReaders, - IProgress progress, - CancellationToken cancellationToken); + DataWriter = dataWriter; + ResourceLocator = resourceLocator; + SystemConfiguration = systemConfiguration; + RequestConfiguration = requestConfiguration; + Logger = logger; } - // TODO: Add "CheckFileSize" method (e.g. for Famos). + #endregion - internal class DataWriterController : IDataWriterController - { - #region Constructors - - public DataWriterController( - IDataWriter dataWriter, - Uri resourceLocator, - IReadOnlyDictionary? systemConfiguration, - IReadOnlyDictionary? requestConfiguration, - ILogger logger) - { - DataWriter = dataWriter; - ResourceLocator = resourceLocator; - SystemConfiguration = systemConfiguration; - RequestConfiguration = requestConfiguration; - Logger = logger; - } - - #endregion + #region Properties - #region Properties + private IReadOnlyDictionary? SystemConfiguration { get; } - private IReadOnlyDictionary? SystemConfiguration { get; } + private IReadOnlyDictionary? RequestConfiguration { get; } - private IReadOnlyDictionary? RequestConfiguration { get; } + private IDataWriter DataWriter { get; } - private IDataWriter DataWriter { get; } + private Uri ResourceLocator { get; } - private Uri ResourceLocator { get; } + private ILogger Logger { get; } - private ILogger Logger { get; } + #endregion - #endregion + #region Methods - #region Methods + public async Task InitializeAsync( + ILogger logger, + CancellationToken cancellationToken) + { + var context = new DataWriterContext( + ResourceLocator: ResourceLocator, + SystemConfiguration: SystemConfiguration, + RequestConfiguration: RequestConfiguration); - public async Task InitializeAsync( - ILogger logger, - CancellationToken cancellationToken) - { - var context = new DataWriterContext( - ResourceLocator: ResourceLocator, - SystemConfiguration: SystemConfiguration, - RequestConfiguration: RequestConfiguration); + await DataWriter.SetContextAsync(context, logger, cancellationToken); + } - await DataWriter.SetContextAsync(context, logger, cancellationToken); - } + public async Task WriteAsync( + DateTime begin, + DateTime end, + TimeSpan samplePeriod, + TimeSpan filePeriod, + CatalogItemRequestPipeReader[] catalogItemRequestPipeReaders, + IProgress? progress, + CancellationToken cancellationToken) + { + /* validation */ + if (!catalogItemRequestPipeReaders.Any()) + return; - public async Task WriteAsync( - DateTime begin, - DateTime end, - TimeSpan samplePeriod, - TimeSpan filePeriod, - CatalogItemRequestPipeReader[] catalogItemRequestPipeReaders, - IProgress? progress, - CancellationToken cancellationToken) + foreach (var catalogItemRequestPipeReader in catalogItemRequestPipeReaders) { - /* validation */ - if (!catalogItemRequestPipeReaders.Any()) - return; - - foreach (var catalogItemRequestPipeReader in catalogItemRequestPipeReaders) - { - if (catalogItemRequestPipeReader.Request.Item.Representation.SamplePeriod != samplePeriod) - throw new ValidationException("All representations must be of the same sample period."); - } + if (catalogItemRequestPipeReader.Request.Item.Representation.SamplePeriod != samplePeriod) + throw new ValidationException("All representations must be of the same sample period."); + } - DataWriterController.ValidateParameters(begin, samplePeriod, filePeriod); + DataWriterController.ValidateParameters(begin, samplePeriod, filePeriod); - /* periods */ - var totalPeriod = end - begin; - Logger.LogDebug("The total period is {TotalPeriod}", totalPeriod); + /* periods */ + var totalPeriod = end - begin; + Logger.LogDebug("The total period is {TotalPeriod}", totalPeriod); - var consumedPeriod = TimeSpan.Zero; - var currentPeriod = default(TimeSpan); + var consumedPeriod = TimeSpan.Zero; + var currentPeriod = default(TimeSpan); - /* progress */ - var dataWriterProgress = new Progress(); + /* progress */ + var dataWriterProgress = new Progress(); - /* no need to remove handler because of short lifetime of IDataWriter */ - dataWriterProgress.ProgressChanged += (sender, progressValue) => - { - var baseProgress = consumedPeriod.Ticks / (double)totalPeriod.Ticks; - var relativeProgressFactor = currentPeriod.Ticks / (double)totalPeriod.Ticks; - var relativeProgress = progressValue * relativeProgressFactor; - progress?.Report(baseProgress + relativeProgress); - }; - - /* catalog items */ - var catalogItems = catalogItemRequestPipeReaders - .Select(catalogItemRequestPipeReader => catalogItemRequestPipeReader.Request.Item) - .ToArray(); - - /* go */ - var lastFileBegin = default(DateTime); - - await NexusUtilities.FileLoopAsync(begin, end, filePeriod, - async (fileBegin, fileOffset, duration) => + /* no need to remove handler because of short lifetime of IDataWriter */ + dataWriterProgress.ProgressChanged += (sender, progressValue) => + { + var baseProgress = consumedPeriod.Ticks / (double)totalPeriod.Ticks; + var relativeProgressFactor = currentPeriod.Ticks / (double)totalPeriod.Ticks; + var relativeProgress = progressValue * relativeProgressFactor; + progress?.Report(baseProgress + relativeProgress); + }; + + /* catalog items */ + var catalogItems = catalogItemRequestPipeReaders + .Select(catalogItemRequestPipeReader => catalogItemRequestPipeReader.Request.Item) + .ToArray(); + + /* go */ + var lastFileBegin = default(DateTime); + + await NexusUtilities.FileLoopAsync(begin, end, filePeriod, + async (fileBegin, fileOffset, duration) => + { + /* Concept: It never happens that the data of a read operation is spreaded over + * multiple buffers. However, it may happen that the data of multiple read + * operations are copied into a single buffer (important to ensure that multiple + * bytes of a single value are always copied together). When the first buffer + * is (partially) read, call the "PipeReader.Advance" function to tell the pipe + * the number of bytes we have consumed. This way we slice our way through + * the buffers so it is OK to only ever read the first buffer of a read result. + */ + + cancellationToken.ThrowIfCancellationRequested(); + + var currentBegin = fileBegin + fileOffset; + Logger.LogTrace("Process period {CurrentBegin} to {CurrentEnd}", currentBegin, currentBegin + duration); + + /* close / open */ + if (fileBegin != lastFileBegin) { - /* Concept: It never happens that the data of a read operation is spreaded over - * multiple buffers. However, it may happen that the data of multiple read - * operations are copied into a single buffer (important to ensure that multiple - * bytes of a single value are always copied together). When the first buffer - * is (partially) read, call the "PipeReader.Advance" function to tell the pipe - * the number of bytes we have consumed. This way we slice our way through - * the buffers so it is OK to only ever read the first buffer of a read result. - */ - - cancellationToken.ThrowIfCancellationRequested(); - - var currentBegin = fileBegin + fileOffset; - Logger.LogTrace("Process period {CurrentBegin} to {CurrentEnd}", currentBegin, currentBegin + duration); - - /* close / open */ - if (fileBegin != lastFileBegin) - { - /* close */ - if (lastFileBegin != default) - await DataWriter.CloseAsync(cancellationToken); - - /* open */ - await DataWriter.OpenAsync( - fileBegin, - filePeriod, - samplePeriod, - catalogItems, - cancellationToken); - } - - lastFileBegin = fileBegin; - - /* loop */ - var consumedFilePeriod = TimeSpan.Zero; - var remainingPeriod = duration; + /* close */ + if (lastFileBegin != default) + await DataWriter.CloseAsync(cancellationToken); + + /* open */ + await DataWriter.OpenAsync( + fileBegin, + filePeriod, + samplePeriod, + catalogItems, + cancellationToken); + } - while (remainingPeriod > TimeSpan.Zero) - { - /* read */ - var readResultTasks = catalogItemRequestPipeReaders - .Select(catalogItemRequestPipeReader => catalogItemRequestPipeReader.DataReader.ReadAsync(cancellationToken)) - .ToArray(); + lastFileBegin = fileBegin; - var readResults = await NexusUtilities.WhenAll(readResultTasks); - var bufferPeriod = readResults.Min(readResult => readResult.Buffer.First.Cast().Length) * samplePeriod; + /* loop */ + var consumedFilePeriod = TimeSpan.Zero; + var remainingPeriod = duration; - if (bufferPeriod == default) - throw new ValidationException("The pipe is empty."); + while (remainingPeriod > TimeSpan.Zero) + { + /* read */ + var readResultTasks = catalogItemRequestPipeReaders + .Select(catalogItemRequestPipeReader => catalogItemRequestPipeReader.DataReader.ReadAsync(cancellationToken)) + .ToArray(); - /* write */ - currentPeriod = new TimeSpan(Math.Min(remainingPeriod.Ticks, bufferPeriod.Ticks)); - var currentLength = (int)(currentPeriod.Ticks / samplePeriod.Ticks); + var readResults = await NexusUtilities.WhenAll(readResultTasks); + var bufferPeriod = readResults.Min(readResult => readResult.Buffer.First.Cast().Length) * samplePeriod; - var requests = catalogItemRequestPipeReaders.Zip(readResults).Select(zipped => - { - var (catalogItemRequestPipeReader, readResult) = zipped; + if (bufferPeriod == default) + throw new ValidationException("The pipe is empty."); - var request = catalogItemRequestPipeReader.Request; - var catalogItem = request.Item; + /* write */ + currentPeriod = new TimeSpan(Math.Min(remainingPeriod.Ticks, bufferPeriod.Ticks)); + var currentLength = (int)(currentPeriod.Ticks / samplePeriod.Ticks); - if (request.BaseItem is not null) - { - var originalResource = request.Item.Resource; + var requests = catalogItemRequestPipeReaders.Zip(readResults).Select(zipped => + { + var (catalogItemRequestPipeReader, readResult) = zipped; - var newResource = new ResourceBuilder(originalResource.Id) - .WithProperty(DataModelExtensions.BasePathKey, request.BaseItem.ToPath()) - .Build(); + var request = catalogItemRequestPipeReader.Request; + var catalogItem = request.Item; - var augmentedResource = originalResource.Merge(newResource); + if (request.BaseItem is not null) + { + var originalResource = request.Item.Resource; - catalogItem = request.Item with - { - Resource = augmentedResource - }; - } + var newResource = new ResourceBuilder(originalResource.Id) + .WithProperty(DataModelExtensions.BasePathKey, request.BaseItem.ToPath()) + .Build(); - var writeRequest = new WriteRequest( - catalogItem, - readResult.Buffer.First.Cast()[..currentLength]); + var augmentedResource = originalResource.Merge(newResource); - return writeRequest; - }).ToArray(); + catalogItem = request.Item with + { + Resource = augmentedResource + }; + } - await DataWriter.WriteAsync( - fileOffset + consumedFilePeriod, - requests, - dataWriterProgress, - cancellationToken); + var writeRequest = new WriteRequest( + catalogItem, + readResult.Buffer.First.Cast()[..currentLength]); - /* advance */ - foreach (var ((_, dataReader), readResult) in catalogItemRequestPipeReaders.Zip(readResults)) - { - dataReader.AdvanceTo(readResult.Buffer.GetPosition(currentLength * sizeof(double))); - } + return writeRequest; + }).ToArray(); - /* update loop state */ - consumedPeriod += currentPeriod; - consumedFilePeriod += currentPeriod; - remainingPeriod -= currentPeriod; + await DataWriter.WriteAsync( + fileOffset + consumedFilePeriod, + requests, + dataWriterProgress, + cancellationToken); - progress?.Report(consumedPeriod.Ticks / (double)totalPeriod.Ticks); + /* advance */ + foreach (var ((_, dataReader), readResult) in catalogItemRequestPipeReaders.Zip(readResults)) + { + dataReader.AdvanceTo(readResult.Buffer.GetPosition(currentLength * sizeof(double))); } - }); - /* close */ - await DataWriter.CloseAsync(cancellationToken); + /* update loop state */ + consumedPeriod += currentPeriod; + consumedFilePeriod += currentPeriod; + remainingPeriod -= currentPeriod; - foreach (var (_, dataReader) in catalogItemRequestPipeReaders) - { - await dataReader.CompleteAsync(); + progress?.Report(consumedPeriod.Ticks / (double)totalPeriod.Ticks); } - } + }); - private static void ValidateParameters( - DateTime begin, - TimeSpan samplePeriod, - TimeSpan filePeriod) - { - if (begin.Ticks % samplePeriod.Ticks != 0) - throw new ValidationException("The begin parameter must be a multiple of the sample period."); + /* close */ + await DataWriter.CloseAsync(cancellationToken); - if (filePeriod.Ticks % samplePeriod.Ticks != 0) - throw new ValidationException("The file period parameter must be a multiple of the sample period."); + foreach (var (_, dataReader) in catalogItemRequestPipeReaders) + { + await dataReader.CompleteAsync(); } + } - #endregion + private static void ValidateParameters( + DateTime begin, + TimeSpan samplePeriod, + TimeSpan filePeriod) + { + if (begin.Ticks % samplePeriod.Ticks != 0) + throw new ValidationException("The begin parameter must be a multiple of the sample period."); + + if (filePeriod.Ticks % samplePeriod.Ticks != 0) + throw new ValidationException("The file period parameter must be a multiple of the sample period."); + } + + #endregion - #region IDisposable + #region IDisposable - private bool _disposedValue; + private bool _disposedValue; - protected virtual void Dispose(bool disposing) + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) { - if (!_disposedValue) + if (disposing) { - if (disposing) - { - var disposable = DataWriter as IDisposable; - disposable?.Dispose(); - } - - _disposedValue = true; + var disposable = DataWriter as IDisposable; + disposable?.Dispose(); } - } - public void Dispose() - { - Dispose(disposing: true); - GC.SuppressFinalize(this); + _disposedValue = true; } + } - #endregion + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); } + + #endregion } \ No newline at end of file diff --git a/src/Nexus/Extensibility/DataWriter/DataWriterControllerTypes.cs b/src/Nexus/Extensibility/DataWriter/DataWriterControllerTypes.cs index 5dbf0c03..879808fc 100644 --- a/src/Nexus/Extensibility/DataWriter/DataWriterControllerTypes.cs +++ b/src/Nexus/Extensibility/DataWriter/DataWriterControllerTypes.cs @@ -1,9 +1,8 @@ using Nexus.Core; using System.IO.Pipelines; -namespace Nexus.Extensibility -{ - internal record CatalogItemRequestPipeReader( - CatalogItemRequest Request, - PipeReader DataReader); -} +namespace Nexus.Extensibility; + +internal record CatalogItemRequestPipeReader( + CatalogItemRequest Request, + PipeReader DataReader); diff --git a/src/Nexus/Extensions/Sources/Sample.cs b/src/Nexus/Extensions/Sources/Sample.cs index 4748285c..d8bd6217 100644 --- a/src/Nexus/Extensions/Sources/Sample.cs +++ b/src/Nexus/Extensions/Sources/Sample.cs @@ -2,211 +2,211 @@ using Nexus.Extensibility; using System.Runtime.InteropServices; -namespace Nexus.Sources +namespace Nexus.Sources; + +[ExtensionDescription( + "Provides catalogs with sample data.", + "https://github.com/malstroem-labs/nexus", + "https://github.com/malstroem-labs/nexus/blob/master/src/Nexus/Extensions/Sources/Sample.cs")] +internal class Sample : IDataSource { - [ExtensionDescription( - "Provides catalogs with sample data.", - "https://github.com/malstroem-labs/nexus", - "https://github.com/malstroem-labs/nexus/blob/master/src/Nexus/Extensions/Sources/Sample.cs")] - internal class Sample : IDataSource - { - #region Fields + #region Fields - public static Guid RegistrationId = new("c2c724ab-9002-4879-9cd9-2147844bee96"); + public static Guid RegistrationId = new("c2c724ab-9002-4879-9cd9-2147844bee96"); - private static readonly double[] DATA = new double[] - { - 6.5, 6.7, 7.9, 8.1, 7.5, 7.6, 7.0, 6.5, 6.0, 5.9, - 5.8, 5.2, 4.6, 5.0, 5.1, 4.9, 5.3, 5.8, 5.9, 6.1, - 5.9, 6.3, 6.5, 6.9, 7.1, 6.9, 7.1, 7.2, 7.6, 7.9, - 8.2, 8.1, 8.2, 8.0, 7.5, 7.7, 7.6, 8.0, 7.5, 7.2, - 6.8, 6.5, 6.6, 6.6, 6.7, 6.2, 5.9, 5.7, 5.9, 6.3, - 6.6, 6.7, 6.9, 6.5, 6.0, 5.8, 5.3, 5.8, 6.1, 6.8 - }; + private static readonly double[] DATA = new double[] + { + 6.5, 6.7, 7.9, 8.1, 7.5, 7.6, 7.0, 6.5, 6.0, 5.9, + 5.8, 5.2, 4.6, 5.0, 5.1, 4.9, 5.3, 5.8, 5.9, 6.1, + 5.9, 6.3, 6.5, 6.9, 7.1, 6.9, 7.1, 7.2, 7.6, 7.9, + 8.2, 8.1, 8.2, 8.0, 7.5, 7.7, 7.6, 8.0, 7.5, 7.2, + 6.8, 6.5, 6.6, 6.6, 6.7, 6.2, 5.9, 5.7, 5.9, 6.3, + 6.6, 6.7, 6.9, 6.5, 6.0, 5.8, 5.3, 5.8, 6.1, 6.8 + }; - public const string LocalCatalogId = "/SAMPLE/LOCAL"; - public const string RemoteCatalogId = "/SAMPLE/REMOTE"; + public const string LocalCatalogId = "/SAMPLE/LOCAL"; + public const string RemoteCatalogId = "/SAMPLE/REMOTE"; - private const string LocalCatalogTitle = "Simulates a local catalog"; - private const string RemoteCatalogTitle = "Simulates a remote catalog"; + private const string LocalCatalogTitle = "Simulates a local catalog"; + private const string RemoteCatalogTitle = "Simulates a remote catalog"; - public const string RemoteUsername = "test"; - public const string RemotePassword = "1234"; + public const string RemoteUsername = "test"; + public const string RemotePassword = "1234"; - #endregion + #endregion - #region Properties + #region Properties - private DataSourceContext Context { get; set; } = default!; + private DataSourceContext Context { get; set; } = default!; - #endregion + #endregion - #region Methods + #region Methods - public Task SetContextAsync( - DataSourceContext context, - ILogger logger, - CancellationToken cancellationToken) - { - Context = context; - return Task.CompletedTask; - } + public Task SetContextAsync( + DataSourceContext context, + ILogger logger, + CancellationToken cancellationToken) + { + Context = context; + return Task.CompletedTask; + } - public Task GetCatalogRegistrationsAsync( - string path, - CancellationToken cancellationToken) - { - if (path == "/") - return Task.FromResult(new CatalogRegistration[] - { - new CatalogRegistration(LocalCatalogId, LocalCatalogTitle), - new CatalogRegistration(RemoteCatalogId, RemoteCatalogTitle), - }); + public Task GetCatalogRegistrationsAsync( + string path, + CancellationToken cancellationToken) + { + if (path == "/") + return Task.FromResult(new CatalogRegistration[] + { + new CatalogRegistration(LocalCatalogId, LocalCatalogTitle), + new CatalogRegistration(RemoteCatalogId, RemoteCatalogTitle), + }); - else - return Task.FromResult(Array.Empty()); - } + else + return Task.FromResult(Array.Empty()); + } - public Task GetCatalogAsync( - string catalogId, - CancellationToken cancellationToken) - { - return Task.FromResult(Sample.LoadCatalog(catalogId)); - } + public Task GetCatalogAsync( + string catalogId, + CancellationToken cancellationToken) + { + return Task.FromResult(Sample.LoadCatalog(catalogId)); + } - public Task<(DateTime Begin, DateTime End)> GetTimeRangeAsync( - string catalogId, - CancellationToken cancellationToken) - { - return Task.FromResult((DateTime.MinValue, DateTime.MaxValue)); - } + public Task<(DateTime Begin, DateTime End)> GetTimeRangeAsync( + string catalogId, + CancellationToken cancellationToken) + { + return Task.FromResult((DateTime.MinValue, DateTime.MaxValue)); + } - public Task GetAvailabilityAsync( - string catalogId, - DateTime begin, - DateTime end, - CancellationToken cancellationToken) - { - return Task.FromResult(1.0); - } + public Task GetAvailabilityAsync( + string catalogId, + DateTime begin, + DateTime end, + CancellationToken cancellationToken) + { + return Task.FromResult(1.0); + } - public async Task ReadAsync( - DateTime begin, - DateTime end, - ReadRequest[] requests, - ReadDataHandler readData, - IProgress progress, - CancellationToken cancellationToken) + public async Task ReadAsync( + DateTime begin, + DateTime end, + ReadRequest[] requests, + ReadDataHandler readData, + IProgress progress, + CancellationToken cancellationToken) + { + var tasks = requests.Select(request => { - var tasks = requests.Select(request => + var (catalogItem, data, status) = request; + + return Task.Run(() => { - var (catalogItem, data, status) = request; + cancellationToken.ThrowIfCancellationRequested(); - return Task.Run(() => + var (catalog, resource, representation, parameters) = catalogItem; + + // check credentials + if (catalog.Id == RemoteCatalogId) { - cancellationToken.ThrowIfCancellationRequested(); + var user = Context.RequestConfiguration?.GetStringValue($"{typeof(Sample).FullName}/user"); + var password = Context.RequestConfiguration?.GetStringValue($"{typeof(Sample).FullName}/password"); - var (catalog, resource, representation, parameters) = catalogItem; + if (user != RemoteUsername || password != RemotePassword) + throw new Exception("The provided credentials are invalid."); + } - // check credentials - if (catalog.Id == RemoteCatalogId) - { - var user = Context.RequestConfiguration?.GetStringValue($"{typeof(Sample).FullName}/user"); - var password = Context.RequestConfiguration?.GetStringValue($"{typeof(Sample).FullName}/password"); + double[] dataDouble; - if (user != RemoteUsername || password != RemotePassword) - throw new Exception("The provided credentials are invalid."); - } + var beginTime = ToUnixTimeStamp(begin); + var endTime = ToUnixTimeStamp(end); + var elementCount = data.Length / representation.ElementSize; - double[] dataDouble; + // unit time + if (resource.Id.Contains("unix_time")) + { + var dt = representation.SamplePeriod.TotalSeconds; + dataDouble = Enumerable.Range(0, elementCount).Select(i => i * dt + beginTime).ToArray(); + } - var beginTime = ToUnixTimeStamp(begin); - var endTime = ToUnixTimeStamp(end); - var elementCount = data.Length / representation.ElementSize; + // temperature or wind speed + else + { + var offset = (long)beginTime; + var dataLength = DATA.Length; - // unit time - if (resource.Id.Contains("unix_time")) - { - var dt = representation.SamplePeriod.TotalSeconds; - dataDouble = Enumerable.Range(0, elementCount).Select(i => i * dt + beginTime).ToArray(); - } + dataDouble = new double[elementCount]; - // temperature or wind speed - else + for (int i = 0; i < elementCount; i++) { - var offset = (long)beginTime; - var dataLength = DATA.Length; - - dataDouble = new double[elementCount]; - - for (int i = 0; i < elementCount; i++) - { - dataDouble[i] = DATA[(offset + i) % dataLength]; - } + dataDouble[i] = DATA[(offset + i) % dataLength]; } + } - MemoryMarshal - .AsBytes(dataDouble.AsSpan()) - .CopyTo(data.Span); + MemoryMarshal + .AsBytes(dataDouble.AsSpan()) + .CopyTo(data.Span); - status.Span - .Fill(1); - }); - }).ToList(); + status.Span + .Fill(1); + }); + }).ToList(); - var finishedTasks = 0; + var finishedTasks = 0; - while (tasks.Any()) - { - var task = await Task.WhenAny(tasks); - cancellationToken.ThrowIfCancellationRequested(); + while (tasks.Any()) + { + var task = await Task.WhenAny(tasks); + cancellationToken.ThrowIfCancellationRequested(); - if (task.Exception is not null && task.Exception.InnerException is not null) - throw task.Exception.InnerException; + if (task.Exception is not null && task.Exception.InnerException is not null) + throw task.Exception.InnerException; - finishedTasks++; - progress.Report(finishedTasks / (double)requests.Length); - tasks.Remove(task); - } + finishedTasks++; + progress.Report(finishedTasks / (double)requests.Length); + tasks.Remove(task); } + } - internal static ResourceCatalog LoadCatalog( - string catalogId) + internal static ResourceCatalog LoadCatalog( + string catalogId) + { + var resourceBuilderA = new ResourceBuilder(id: "T1") + .WithUnit("°C") + .WithDescription("Test Resource A") + .WithGroups("Group 1") + .AddRepresentation(new Representation(dataType: NexusDataType.FLOAT64, samplePeriod: TimeSpan.FromSeconds(1))); + + var resourceBuilderB = new ResourceBuilder(id: "V1") + .WithUnit("m/s") + .WithDescription("Test Resource B") + .WithGroups("Group 1") + .AddRepresentation(new Representation(dataType: NexusDataType.FLOAT64, samplePeriod: TimeSpan.FromSeconds(1))); + + var resourceBuilderC = new ResourceBuilder(id: "unix_time1") + .WithDescription("Test Resource C") + .WithGroups("Group 2") + .AddRepresentation(new Representation(dataType: NexusDataType.FLOAT64, samplePeriod: TimeSpan.FromMilliseconds(40))); + + var resourceBuilderD = new ResourceBuilder(id: "unix_time2") + .WithDescription("Test Resource D") + .WithGroups("Group 2") + .AddRepresentation(new Representation(dataType: NexusDataType.FLOAT64, samplePeriod: TimeSpan.FromSeconds(1))); + + var catalogBuilder = new ResourceCatalogBuilder(catalogId); + + catalogBuilder.AddResources(new List() { - var resourceBuilderA = new ResourceBuilder(id: "T1") - .WithUnit("°C") - .WithDescription("Test Resource A") - .WithGroups("Group 1") - .AddRepresentation(new Representation(dataType: NexusDataType.FLOAT64, samplePeriod: TimeSpan.FromSeconds(1))); - - var resourceBuilderB = new ResourceBuilder(id: "V1") - .WithUnit("m/s") - .WithDescription("Test Resource B") - .WithGroups("Group 1") - .AddRepresentation(new Representation(dataType: NexusDataType.FLOAT64, samplePeriod: TimeSpan.FromSeconds(1))); - - var resourceBuilderC = new ResourceBuilder(id: "unix_time1") - .WithDescription("Test Resource C") - .WithGroups("Group 2") - .AddRepresentation(new Representation(dataType: NexusDataType.FLOAT64, samplePeriod: TimeSpan.FromMilliseconds(40))); - - var resourceBuilderD = new ResourceBuilder(id: "unix_time2") - .WithDescription("Test Resource D") - .WithGroups("Group 2") - .AddRepresentation(new Representation(dataType: NexusDataType.FLOAT64, samplePeriod: TimeSpan.FromSeconds(1))); - - var catalogBuilder = new ResourceCatalogBuilder(catalogId); - - catalogBuilder.AddResources(new List() - { - resourceBuilderA.Build(), - resourceBuilderB.Build(), - resourceBuilderC.Build(), - resourceBuilderD.Build() - }); - - if (catalogId == RemoteCatalogId) - catalogBuilder.WithReadme( + resourceBuilderA.Build(), + resourceBuilderB.Build(), + resourceBuilderC.Build(), + resourceBuilderD.Build() + }); + + if (catalogId == RemoteCatalogId) + catalogBuilder.WithReadme( @"This catalog demonstrates how to access data sources that require additional credentials. These can be appended in the user settings menu (on the top right). In case of this example catalog, the JSON string to be added would look like the following: ```json @@ -221,15 +221,14 @@ internal static ResourceCatalog LoadCatalog( As soon as these credentials have been added, you should be granted full access to the data. "); - return catalogBuilder.Build(); - } - - private static double ToUnixTimeStamp( - DateTime value) - { - return value.Subtract(new DateTime(1970, 1, 1)).TotalSeconds; - } + return catalogBuilder.Build(); + } - #endregion + private static double ToUnixTimeStamp( + DateTime value) + { + return value.Subtract(new DateTime(1970, 1, 1)).TotalSeconds; } + + #endregion } diff --git a/src/Nexus/Extensions/Writers/Csv.cs b/src/Nexus/Extensions/Writers/Csv.cs index 2142d29e..30f77a9f 100644 --- a/src/Nexus/Extensions/Writers/Csv.cs +++ b/src/Nexus/Extensions/Writers/Csv.cs @@ -13,350 +13,349 @@ // Why not CSV on the web? https://twitter.com/readdavid/status/1195315653449793536 // Linting: https://csvlint.io/ and https://ruby-rdf.github.io/rdf-tabular/ -namespace Nexus.Writers +namespace Nexus.Writers; + +[DataWriterDescription(DESCRIPTION)] + +[ExtensionDescription( + "Exports comma-separated values following the frictionless data standard", + "https://github.com/malstroem-labs/nexus", + "https://github.com/malstroem-labs/nexus/blob/master/src/Nexus/Extensions/Writers/Csv.cs")] +internal class Csv : IDataWriter, IDisposable { - [DataWriterDescription(DESCRIPTION)] + #region "Fields" - [ExtensionDescription( - "Exports comma-separated values following the frictionless data standard", - "https://github.com/malstroem-labs/nexus", - "https://github.com/malstroem-labs/nexus/blob/master/src/Nexus/Extensions/Writers/Csv.cs")] - internal class Csv : IDataWriter, IDisposable + private const string DESCRIPTION = """ { - #region "Fields" - - private const string DESCRIPTION = """ - { - "label":"CSV + Schema (*.csv)", - "options":{ - "row-index-format":{ - "type":"select", - "label":"Row index format", - "default":"excel", - "items":{ - "excel":"Excel time", - "index":"Index-based", - "unix":"Unix time", - "iso-8601":"ISO 8601" - } - }, - "significant-figures":{ - "type":"input-integer", - "label":"Significant figures", - "default":4, - "minimum":0, - "maximum":30 + "label":"CSV + Schema (*.csv)", + "options":{ + "row-index-format":{ + "type":"select", + "label":"Row index format", + "default":"excel", + "items":{ + "excel":"Excel time", + "index":"Index-based", + "unix":"Unix time", + "iso-8601":"ISO 8601" } + }, + "significant-figures":{ + "type":"input-integer", + "label":"Significant figures", + "default":4, + "minimum":0, + "maximum":30 } } - """; + } + """; - private static readonly DateTime _unixEpoch = new(1970, 01, 01); + private static readonly DateTime _unixEpoch = new(1970, 01, 01); - private static readonly NumberFormatInfo _nfi = new() - { - NumberDecimalSeparator = ".", - NumberGroupSeparator = string.Empty - }; + private static readonly NumberFormatInfo _nfi = new() + { + NumberDecimalSeparator = ".", + NumberGroupSeparator = string.Empty + }; - private static readonly JsonSerializerOptions _options = new() - { - WriteIndented = true, - Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull - }; + private static readonly JsonSerializerOptions _options = new() + { + WriteIndented = true, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }; - private double _unixStart; - private double _excelStart; - private DateTime _lastFileBegin; - private TimeSpan _lastSamplePeriod; - private readonly Dictionary _resourceMap = new(); + private double _unixStart; + private double _excelStart; + private DateTime _lastFileBegin; + private TimeSpan _lastSamplePeriod; + private readonly Dictionary _resourceMap = new(); - #endregion + #endregion - #region Properties + #region Properties - private DataWriterContext Context { get; set; } = default!; + private DataWriterContext Context { get; set; } = default!; - #endregion + #endregion - #region "Methods" + #region "Methods" - public Task SetContextAsync( - DataWriterContext context, - ILogger logger, - CancellationToken cancellationToken) - { - Context = context; - return Task.CompletedTask; - } + public Task SetContextAsync( + DataWriterContext context, + ILogger logger, + CancellationToken cancellationToken) + { + Context = context; + return Task.CompletedTask; + } - public async Task OpenAsync( - DateTime fileBegin, - TimeSpan filePeriod, - TimeSpan samplePeriod, - CatalogItem[] catalogItems, - CancellationToken cancellationToken) + public async Task OpenAsync( + DateTime fileBegin, + TimeSpan filePeriod, + TimeSpan samplePeriod, + CatalogItem[] catalogItems, + CancellationToken cancellationToken) + { + _lastFileBegin = fileBegin; + _lastSamplePeriod = samplePeriod; + _unixStart = (fileBegin - _unixEpoch).TotalSeconds; + _excelStart = fileBegin.ToOADate(); + + foreach (var catalogItemGroup in catalogItems.GroupBy(catalogItem => catalogItem.Catalog)) { - _lastFileBegin = fileBegin; - _lastSamplePeriod = samplePeriod; - _unixStart = (fileBegin - _unixEpoch).TotalSeconds; - _excelStart = fileBegin.ToOADate(); + cancellationToken.ThrowIfCancellationRequested(); - foreach (var catalogItemGroup in catalogItems.GroupBy(catalogItem => catalogItem.Catalog)) - { - cancellationToken.ThrowIfCancellationRequested(); + var catalog = catalogItemGroup.Key; + var physicalId = catalog.Id.TrimStart('/').Replace('/', '_'); + var root = Context.ResourceLocator.ToPath(); - var catalog = catalogItemGroup.Key; - var physicalId = catalog.Id.TrimStart('/').Replace('/', '_'); - var root = Context.ResourceLocator.ToPath(); + /* metadata */ + var resourceFileNameWithoutExtension = $"{physicalId}_{samplePeriod.ToUnitString()}"; + var resourceFileName = $"{resourceFileNameWithoutExtension}.resource.json"; + var resourceFilePath = Path.Combine(root, resourceFileName); - /* metadata */ - var resourceFileNameWithoutExtension = $"{physicalId}_{samplePeriod.ToUnitString()}"; - var resourceFileName = $"{resourceFileNameWithoutExtension}.resource.json"; - var resourceFilePath = Path.Combine(root, resourceFileName); + if (!_resourceMap.TryGetValue(resourceFilePath, out var resource)) + { + var rowIndexFormat = Context.RequestConfiguration?.GetStringValue("row-index-format") ?? "index"; + var constraints = new Constraints(Required: true); - if (!_resourceMap.TryGetValue(resourceFilePath, out var resource)) + var timestampField = rowIndexFormat switch { - var rowIndexFormat = Context.RequestConfiguration?.GetStringValue("row-index-format") ?? "index"; - var constraints = new Constraints(Required: true); - - var timestampField = rowIndexFormat switch - { - "index" => new Field("Index", "integer", constraints, default), - "unix" => new Field("Unix time", "number", constraints, default), - "excel" => new Field("Excel time", "number", constraints, default), - "iso-8601" => new Field("ISO 8601 time", "datetime", constraints, default), - _ => throw new NotSupportedException($"The row index format {rowIndexFormat} is not supported.") - }; - - var layout = new Layout() - { - HeaderRows = new[] { 4 } - }; - - var fields = new[] { timestampField }.Concat(catalogItemGroup.Select(catalogItem => - { - var fieldName = GetFieldName(catalogItem); - - return new Field( - Name: fieldName, - Type: "number", - Constraints: constraints, - Properties: catalogItem.Resource.Properties); - })).ToArray(); - - var schema = new Schema( - PrimaryKey: timestampField.Name, - Fields: fields, - Properties: catalog.Properties - ); - - resource = new CsvResource( - Encoding: "utf-8-sig", - Format: "csv", - Hashing: "md5", - Name: resourceFileNameWithoutExtension.ToLower(), - Profile: "tabular-data-resource", - Scheme: "multipart", - Path: new List(), - Layout: layout, - Schema: schema); - - _resourceMap[resourceFilePath] = resource; - } - - /* data */ - var dataFileName = $"{physicalId}_{ToISO8601(fileBegin)}_{samplePeriod.ToUnitString()}.csv"; - var dataFilePath = Path.Combine(root, dataFileName); - - if (!File.Exists(dataFilePath)) + "index" => new Field("Index", "integer", constraints, default), + "unix" => new Field("Unix time", "number", constraints, default), + "excel" => new Field("Excel time", "number", constraints, default), + "iso-8601" => new Field("ISO 8601 time", "datetime", constraints, default), + _ => throw new NotSupportedException($"The row index format {rowIndexFormat} is not supported.") + }; + + var layout = new Layout() { - var stringBuilder = new StringBuilder(); + HeaderRows = new[] { 4 } + }; - using var streamWriter = new StreamWriter(File.Open(dataFilePath, FileMode.Append, FileAccess.Write), Encoding.UTF8); + var fields = new[] { timestampField }.Concat(catalogItemGroup.Select(catalogItem => + { + var fieldName = GetFieldName(catalogItem); + + return new Field( + Name: fieldName, + Type: "number", + Constraints: constraints, + Properties: catalogItem.Resource.Properties); + })).ToArray(); + + var schema = new Schema( + PrimaryKey: timestampField.Name, + Fields: fields, + Properties: catalog.Properties + ); + + resource = new CsvResource( + Encoding: "utf-8-sig", + Format: "csv", + Hashing: "md5", + Name: resourceFileNameWithoutExtension.ToLower(), + Profile: "tabular-data-resource", + Scheme: "multipart", + Path: new List(), + Layout: layout, + Schema: schema); + + _resourceMap[resourceFilePath] = resource; + } - /* header values */ - // TODO: use .ToString("o") instead? - stringBuilder.Append($"# date_time: {ToISO8601(fileBegin)}"); - AppendWindowsNewLine(stringBuilder); + /* data */ + var dataFileName = $"{physicalId}_{ToISO8601(fileBegin)}_{samplePeriod.ToUnitString()}.csv"; + var dataFilePath = Path.Combine(root, dataFileName); - stringBuilder.Append($"# sample_period: {samplePeriod.ToUnitString()}"); - AppendWindowsNewLine(stringBuilder); + if (!File.Exists(dataFilePath)) + { + var stringBuilder = new StringBuilder(); - stringBuilder.Append($"# catalog_id: {catalog.Id}"); - AppendWindowsNewLine(stringBuilder); + using var streamWriter = new StreamWriter(File.Open(dataFilePath, FileMode.Append, FileAccess.Write), Encoding.UTF8); - /* field name */ - var timestampField = resource.Schema.Fields.First(); - stringBuilder.Append($"{timestampField.Name},"); + /* header values */ + // TODO: use .ToString("o") instead? + stringBuilder.Append($"# date_time: {ToISO8601(fileBegin)}"); + AppendWindowsNewLine(stringBuilder); - foreach (var catalogItem in catalogItemGroup) - { - var fieldName = GetFieldName(catalogItem); - stringBuilder.Append($"{fieldName},"); - } + stringBuilder.Append($"# sample_period: {samplePeriod.ToUnitString()}"); + AppendWindowsNewLine(stringBuilder); - stringBuilder.Remove(stringBuilder.Length - 1, 1); - AppendWindowsNewLine(stringBuilder); + stringBuilder.Append($"# catalog_id: {catalog.Id}"); + AppendWindowsNewLine(stringBuilder); - await streamWriter.WriteAsync(stringBuilder, cancellationToken); + /* field name */ + var timestampField = resource.Schema.Fields.First(); + stringBuilder.Append($"{timestampField.Name},"); - resource.Path.Add(dataFileName); + foreach (var catalogItem in catalogItemGroup) + { + var fieldName = GetFieldName(catalogItem); + stringBuilder.Append($"{fieldName},"); } + + stringBuilder.Remove(stringBuilder.Length - 1, 1); + AppendWindowsNewLine(stringBuilder); + + await streamWriter.WriteAsync(stringBuilder, cancellationToken); + + resource.Path.Add(dataFileName); } } + } - public async Task WriteAsync(TimeSpan fileOffset, WriteRequest[] requests, IProgress progress, CancellationToken cancellationToken) - { - var offset = fileOffset.Ticks / _lastSamplePeriod.Ticks; + public async Task WriteAsync(TimeSpan fileOffset, WriteRequest[] requests, IProgress progress, CancellationToken cancellationToken) + { + var offset = fileOffset.Ticks / _lastSamplePeriod.Ticks; - var requestGroups = requests - .GroupBy(request => request.CatalogItem.Catalog) - .ToList(); + var requestGroups = requests + .GroupBy(request => request.CatalogItem.Catalog) + .ToList(); - var rowIndexFormat = Context.RequestConfiguration?.GetStringValue("row-index-format") ?? "index"; + var rowIndexFormat = Context.RequestConfiguration?.GetStringValue("row-index-format") ?? "index"; - var significantFigures = int.Parse(Context.RequestConfiguration?.GetStringValue("significant-figures") ?? "4"); - significantFigures = Math.Clamp(significantFigures, 0, 30); + var significantFigures = int.Parse(Context.RequestConfiguration?.GetStringValue("significant-figures") ?? "4"); + significantFigures = Math.Clamp(significantFigures, 0, 30); - var groupIndex = 0; - var consumedLength = 0UL; - var stringBuilder = new StringBuilder(); + var groupIndex = 0; + var consumedLength = 0UL; + var stringBuilder = new StringBuilder(); - foreach (var requestGroup in requestGroups) - { - cancellationToken.ThrowIfCancellationRequested(); + foreach (var requestGroup in requestGroups) + { + cancellationToken.ThrowIfCancellationRequested(); - var catalog = requestGroup.Key; - var writeRequests = requestGroup.ToArray(); - var physicalId = catalog.Id.TrimStart('/').Replace('/', '_'); - var root = Context.ResourceLocator.ToPath(); - var filePath = Path.Combine(root, $"{physicalId}_{ToISO8601(_lastFileBegin)}_{_lastSamplePeriod.ToUnitString()}.csv"); + var catalog = requestGroup.Key; + var writeRequests = requestGroup.ToArray(); + var physicalId = catalog.Id.TrimStart('/').Replace('/', '_'); + var root = Context.ResourceLocator.ToPath(); + var filePath = Path.Combine(root, $"{physicalId}_{ToISO8601(_lastFileBegin)}_{_lastSamplePeriod.ToUnitString()}.csv"); - using var streamWriter = new StreamWriter(File.Open(filePath, FileMode.Append, FileAccess.Write), Encoding.UTF8); + using var streamWriter = new StreamWriter(File.Open(filePath, FileMode.Append, FileAccess.Write), Encoding.UTF8); - var dateTimeStart = _lastFileBegin + fileOffset; + var dateTimeStart = _lastFileBegin + fileOffset; - var unixStart = _unixStart + fileOffset.TotalSeconds; - var unixScalingFactor = (double)_lastSamplePeriod.Ticks / TimeSpan.FromSeconds(1).Ticks; + var unixStart = _unixStart + fileOffset.TotalSeconds; + var unixScalingFactor = (double)_lastSamplePeriod.Ticks / TimeSpan.FromSeconds(1).Ticks; - var excelStart = _excelStart + fileOffset.TotalDays; - var excelScalingFactor = (double)_lastSamplePeriod.Ticks / TimeSpan.FromDays(1).Ticks; + var excelStart = _excelStart + fileOffset.TotalDays; + var excelScalingFactor = (double)_lastSamplePeriod.Ticks / TimeSpan.FromDays(1).Ticks; - var rowLength = writeRequests.First().Data.Length; + var rowLength = writeRequests.First().Data.Length; - for (int rowIndex = 0; rowIndex < rowLength; rowIndex++) - { - stringBuilder.Clear(); + for (int rowIndex = 0; rowIndex < rowLength; rowIndex++) + { + stringBuilder.Clear(); - switch (rowIndexFormat) - { - case "index": - stringBuilder.Append($"{string.Format(_nfi, "{0:N0}", offset + rowIndex)},"); - break; + switch (rowIndexFormat) + { + case "index": + stringBuilder.Append($"{string.Format(_nfi, "{0:N0}", offset + rowIndex)},"); + break; - case "unix": - stringBuilder.Append($"{string.Format(_nfi, "{0:N5}", unixStart + rowIndex * unixScalingFactor)},"); - break; + case "unix": + stringBuilder.Append($"{string.Format(_nfi, "{0:N5}", unixStart + rowIndex * unixScalingFactor)},"); + break; - case "excel": - stringBuilder.Append($"{string.Format(_nfi, "{0:N9}", excelStart + rowIndex * excelScalingFactor)},"); - break; + case "excel": + stringBuilder.Append($"{string.Format(_nfi, "{0:N9}", excelStart + rowIndex * excelScalingFactor)},"); + break; - case "iso-8601": - stringBuilder.Append($"{(dateTimeStart + (rowIndex * _lastSamplePeriod)).ToString("o")},"); - break; + case "iso-8601": + stringBuilder.Append($"{(dateTimeStart + (rowIndex * _lastSamplePeriod)).ToString("o")},"); + break; - default: - throw new NotSupportedException($"The row index format {rowIndexFormat} is not supported."); - } + default: + throw new NotSupportedException($"The row index format {rowIndexFormat} is not supported."); + } - for (int i = 0; i < writeRequests.Length; i++) - { - var value = writeRequests[i].Data.Span[rowIndex]; - stringBuilder.Append($"{string.Format(_nfi, $"{{0:G{significantFigures}}}", value)},"); - } + for (int i = 0; i < writeRequests.Length; i++) + { + var value = writeRequests[i].Data.Span[rowIndex]; + stringBuilder.Append($"{string.Format(_nfi, $"{{0:G{significantFigures}}}", value)},"); + } - stringBuilder.Remove(stringBuilder.Length - 1, 1); - AppendWindowsNewLine(stringBuilder); + stringBuilder.Remove(stringBuilder.Length - 1, 1); + AppendWindowsNewLine(stringBuilder); - await streamWriter.WriteAsync(stringBuilder, cancellationToken); + await streamWriter.WriteAsync(stringBuilder, cancellationToken); - consumedLength += (ulong)writeRequests.Length; + consumedLength += (ulong)writeRequests.Length; - if (consumedLength >= 10000) - { - cancellationToken.ThrowIfCancellationRequested(); - progress.Report((groupIndex + rowIndex / (double)rowLength) / requestGroups.Count); - consumedLength = 0; - } + if (consumedLength >= 10000) + { + cancellationToken.ThrowIfCancellationRequested(); + progress.Report((groupIndex + rowIndex / (double)rowLength) / requestGroups.Count); + consumedLength = 0; } - - groupIndex++; } - } - public Task CloseAsync(CancellationToken cancellationToken) - { - return Task.CompletedTask; + groupIndex++; } + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void AppendWindowsNewLine(StringBuilder stringBuilder) - { - stringBuilder.Append("\r\n"); - } + public Task CloseAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } - private static string GetFieldName(CatalogItem catalogItem) - { - var unit = catalogItem.Resource.Properties? - .GetStringValue(DataModelExtensions.UnitKey); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void AppendWindowsNewLine(StringBuilder stringBuilder) + { + stringBuilder.Append("\r\n"); + } - var fieldName = $"{catalogItem.Resource.Id}_{catalogItem.Representation.Id}{DataModelUtilities.GetRepresentationParameterString(catalogItem.Parameters)}"; + private static string GetFieldName(CatalogItem catalogItem) + { + var unit = catalogItem.Resource.Properties? + .GetStringValue(DataModelExtensions.UnitKey); - fieldName += unit is null - ? "" - : $" ({unit})"; + var fieldName = $"{catalogItem.Resource.Id}_{catalogItem.Representation.Id}{DataModelUtilities.GetRepresentationParameterString(catalogItem.Parameters)}"; - return fieldName; - } + fieldName += unit is null + ? "" + : $" ({unit})"; - private static string ToISO8601(DateTime dateTime) - { - return dateTime.ToUniversalTime().ToString("yyyy-MM-ddTHH-mm-ssZ"); - } + return fieldName; + } + + private static string ToISO8601(DateTime dateTime) + { + return dateTime.ToUniversalTime().ToString("yyyy-MM-ddTHH-mm-ssZ"); + } - #endregion + #endregion - #region IDisposable + #region IDisposable - private bool _disposedValue; + private bool _disposedValue; - protected virtual void Dispose(bool disposing) + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) { - if (!_disposedValue) + if (disposing) { - if (disposing) + foreach (var (path, resource) in _resourceMap) { - foreach (var (path, resource) in _resourceMap) - { - var jsonString = JsonSerializer.Serialize(resource, _options); - File.WriteAllText(path, jsonString); - } + var jsonString = JsonSerializer.Serialize(resource, _options); + File.WriteAllText(path, jsonString); } - - _disposedValue = true; } - } - public void Dispose() - { - Dispose(disposing: true); - GC.SuppressFinalize(this); + _disposedValue = true; } + } - #endregion + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); } + + #endregion } diff --git a/src/Nexus/Extensions/Writers/CsvTypes.cs b/src/Nexus/Extensions/Writers/CsvTypes.cs index d9f7515c..1e2a6a30 100644 --- a/src/Nexus/Extensions/Writers/CsvTypes.cs +++ b/src/Nexus/Extensions/Writers/CsvTypes.cs @@ -1,32 +1,31 @@ using System.Text.Json; -namespace Nexus.Writers -{ - internal record struct Layout( - int[] HeaderRows); +namespace Nexus.Writers; - internal record struct Constraints( - bool Required); +internal record struct Layout( + int[] HeaderRows); - internal record struct Field( - string Name, - string Type, - Constraints Constraints, - IReadOnlyDictionary? Properties); +internal record struct Constraints( + bool Required); - internal record struct Schema( - string PrimaryKey, - Field[] Fields, - IReadOnlyDictionary? Properties); +internal record struct Field( + string Name, + string Type, + Constraints Constraints, + IReadOnlyDictionary? Properties); - internal record struct CsvResource( - string Encoding, - string Format, - string Hashing, - string Name, - string Profile, - string Scheme, - List Path, - Layout Layout, - Schema Schema); -} +internal record struct Schema( + string PrimaryKey, + Field[] Fields, + IReadOnlyDictionary? Properties); + +internal record struct CsvResource( + string Encoding, + string Format, + string Hashing, + string Name, + string Profile, + string Scheme, + List Path, + Layout Layout, + Schema Schema); diff --git a/src/Nexus/PackageManagement/PackageController.cs b/src/Nexus/PackageManagement/PackageController.cs index c8000245..129ef841 100644 --- a/src/Nexus/PackageManagement/PackageController.cs +++ b/src/Nexus/PackageManagement/PackageController.cs @@ -10,929 +10,928 @@ using ICSharpCode.SharpZipLib.Tar; using Nexus.Core; -namespace Nexus.PackageManagement +namespace Nexus.PackageManagement; + +internal partial class PackageController { - internal partial class PackageController - { - #region Fields + #region Fields - public static Guid BUILTIN_ID = new("97d297d2-df6f-4c85-9d07-86bc64a041a6"); - public const string BUILTIN_PROVIDER = "nexus"; + public static Guid BUILTIN_ID = new("97d297d2-df6f-4c85-9d07-86bc64a041a6"); + public const string BUILTIN_PROVIDER = "nexus"; - private const int MAX_PAGES = 20; - private const int PER_PAGE = 100; + private const int MAX_PAGES = 20; + private const int PER_PAGE = 100; - private static readonly HttpClient _httpClient = new(); + private static readonly HttpClient _httpClient = new(); - private readonly ILogger _logger; - private PackageLoadContext? _loadContext; + private readonly ILogger _logger; + private PackageLoadContext? _loadContext; - #endregion + #endregion - #region Constructors + #region Constructors - public PackageController(InternalPackageReference packageReference, ILogger logger) - { - PackageReference = packageReference; - _logger = logger; - } + public PackageController(InternalPackageReference packageReference, ILogger logger) + { + PackageReference = packageReference; + _logger = logger; + } - #endregion + #endregion - #region Properties + #region Properties - public InternalPackageReference PackageReference { get; } + public InternalPackageReference PackageReference { get; } - #endregion + #endregion - #region Methods + #region Methods - public async Task DiscoverAsync(CancellationToken cancellationToken) + public async Task DiscoverAsync(CancellationToken cancellationToken) + { + _logger.LogDebug("Discover package versions using provider {Provider}", PackageReference.Provider); + string[] result = PackageReference.Provider switch { - _logger.LogDebug("Discover package versions using provider {Provider}", PackageReference.Provider); - string[] result = PackageReference.Provider switch - { - BUILTIN_PROVIDER => new string[] { "current" }, - "local" => await DiscoverLocalAsync(cancellationToken), - "git-tag" => await DiscoverGitTagsAsync(cancellationToken), - "github-releases" => await DiscoverGithubReleasesAsync(cancellationToken), - "gitlab-packages-generic-v4" => await DiscoverGitLabPackagesGenericAsync(cancellationToken), - /* this approach does not work, see rationale below (#region gitlab-releases-v4) - * "gitlab-releases-v4" => await DiscoverGitLabReleasesAsync(cancellationToken); - */ - _ => throw new ArgumentException($"The provider {PackageReference.Provider} is not supported."), - }; - return result; + BUILTIN_PROVIDER => new string[] { "current" }, + "local" => await DiscoverLocalAsync(cancellationToken), + "git-tag" => await DiscoverGitTagsAsync(cancellationToken), + "github-releases" => await DiscoverGithubReleasesAsync(cancellationToken), + "gitlab-packages-generic-v4" => await DiscoverGitLabPackagesGenericAsync(cancellationToken), + /* this approach does not work, see rationale below (#region gitlab-releases-v4) + * "gitlab-releases-v4" => await DiscoverGitLabReleasesAsync(cancellationToken); + */ + _ => throw new ArgumentException($"The provider {PackageReference.Provider} is not supported."), + }; + return result; + } + + public async Task LoadAsync(string restoreRoot, CancellationToken cancellationToken) + { + if (_loadContext is not null) + throw new Exception("The extension is already loaded."); + + Assembly assembly; + + if (PackageReference.Provider == BUILTIN_PROVIDER) + { + assembly = Assembly.GetExecutingAssembly(); + _loadContext = new PackageLoadContext(assembly.Location); } - public async Task LoadAsync(string restoreRoot, CancellationToken cancellationToken) + else { - if (_loadContext is not null) - throw new Exception("The extension is already loaded."); + var restoreFolderPath = await RestoreAsync(restoreRoot, cancellationToken); + var depsJsonExtension = ".deps.json"; - Assembly assembly; + var depsJsonFilePath = Directory + .EnumerateFiles(restoreFolderPath, $"*{depsJsonExtension}", SearchOption.AllDirectories) + .SingleOrDefault(); - if (PackageReference.Provider == BUILTIN_PROVIDER) - { - assembly = Assembly.GetExecutingAssembly(); - _loadContext = new PackageLoadContext(assembly.Location); - } + if (depsJsonFilePath is null) + throw new Exception($"Could not determine the location of the .deps.json file in folder {restoreFolderPath}."); - else - { - var restoreFolderPath = await RestoreAsync(restoreRoot, cancellationToken); - var depsJsonExtension = ".deps.json"; + var entryDllPath = depsJsonFilePath[..^depsJsonExtension.Length] + ".dll"; - var depsJsonFilePath = Directory - .EnumerateFiles(restoreFolderPath, $"*{depsJsonExtension}", SearchOption.AllDirectories) - .SingleOrDefault(); + if (entryDllPath is null) + throw new Exception($"Could not determine the location of the entry DLL file in folder {restoreFolderPath}."); - if (depsJsonFilePath is null) - throw new Exception($"Could not determine the location of the .deps.json file in folder {restoreFolderPath}."); + _loadContext = new PackageLoadContext(entryDllPath); - var entryDllPath = depsJsonFilePath[..^depsJsonExtension.Length] + ".dll"; + var assemblyName = new AssemblyName(Path.GetFileNameWithoutExtension(entryDllPath)); + assembly = _loadContext.LoadFromAssemblyName(assemblyName); + } - if (entryDllPath is null) - throw new Exception($"Could not determine the location of the entry DLL file in folder {restoreFolderPath}."); + return assembly; + } - _loadContext = new PackageLoadContext(entryDllPath); + public WeakReference Unload() + { + if (_loadContext is null) + throw new Exception("The extension is not yet loaded."); - var assemblyName = new AssemblyName(Path.GetFileNameWithoutExtension(entryDllPath)); - assembly = _loadContext.LoadFromAssemblyName(assemblyName); - } + _loadContext.Unload(); + var weakReference = new WeakReference(_loadContext, trackResurrection: true); + _loadContext = default; - return assembly; - } + return weakReference; + } + + internal async Task RestoreAsync(string restoreRoot, CancellationToken cancellationToken) + { + var actualRestoreRoot = Path.Combine(restoreRoot, PackageReference.Provider); - public WeakReference Unload() + _logger.LogDebug("Restore package to {RestoreRoot} using provider {Provider}", actualRestoreRoot, PackageReference.Provider); + string restoreFolderPath = PackageReference.Provider switch { - if (_loadContext is null) - throw new Exception("The extension is not yet loaded."); + "local" => await RestoreLocalAsync(actualRestoreRoot, cancellationToken), + "git-tag" => await RestoreGitTagAsync(actualRestoreRoot, cancellationToken), + "github-releases" => await RestoreGitHubReleasesAsync(actualRestoreRoot, cancellationToken), + "gitlab-packages-generic-v4" => await RestoreGitLabPackagesGenericAsync(actualRestoreRoot, cancellationToken), + /* this approach does not work, see rationale below (#region gitlab-releases-v4) *///case "gitlab-releases-v4": + // restoreFolderPath = await RestoreGitLabReleasesAsync(actualRestoreRoot, cancellationToken); + // break; + _ => throw new ArgumentException($"The provider {PackageReference.Provider} is not supported."), + }; + return restoreFolderPath; + } - _loadContext.Unload(); - var weakReference = new WeakReference(_loadContext, trackResurrection: true); - _loadContext = default; + private static void CloneFolder(string source, string target) + { + if (!Directory.Exists(source)) + throw new Exception("The source directory does not exist."); - return weakReference; - } + Directory.CreateDirectory(target); - internal async Task RestoreAsync(string restoreRoot, CancellationToken cancellationToken) - { - var actualRestoreRoot = Path.Combine(restoreRoot, PackageReference.Provider); + var sourceInfo = new DirectoryInfo(source); + var targetInfo = new DirectoryInfo(target); - _logger.LogDebug("Restore package to {RestoreRoot} using provider {Provider}", actualRestoreRoot, PackageReference.Provider); - string restoreFolderPath = PackageReference.Provider switch - { - "local" => await RestoreLocalAsync(actualRestoreRoot, cancellationToken), - "git-tag" => await RestoreGitTagAsync(actualRestoreRoot, cancellationToken), - "github-releases" => await RestoreGitHubReleasesAsync(actualRestoreRoot, cancellationToken), - "gitlab-packages-generic-v4" => await RestoreGitLabPackagesGenericAsync(actualRestoreRoot, cancellationToken), - /* this approach does not work, see rationale below (#region gitlab-releases-v4) *///case "gitlab-releases-v4": - // restoreFolderPath = await RestoreGitLabReleasesAsync(actualRestoreRoot, cancellationToken); - // break; - _ => throw new ArgumentException($"The provider {PackageReference.Provider} is not supported."), - }; - return restoreFolderPath; - } + if (sourceInfo.FullName == targetInfo.FullName) + throw new Exception("Source and destination are the same."); - private static void CloneFolder(string source, string target) + foreach (var folderPath in Directory.GetDirectories(source)) { - if (!Directory.Exists(source)) - throw new Exception("The source directory does not exist."); + var folderName = Path.GetFileName(folderPath); - Directory.CreateDirectory(target); + Directory.CreateDirectory(Path.Combine(target, folderName)); + CloneFolder(folderPath, Path.Combine(target, folderName)); + } - var sourceInfo = new DirectoryInfo(source); - var targetInfo = new DirectoryInfo(target); + foreach (var file in Directory.GetFiles(source)) + { + File.Copy(file, Path.Combine(target, Path.GetFileName(file))); + } + } - if (sourceInfo.FullName == targetInfo.FullName) - throw new Exception("Source and destination are the same."); + private static async Task DownloadAndExtractAsync( + string assetName, + string assetUrl, + string targetPath, + Dictionary headers) + { + // get download stream + async Task GetAssetResponseAsync() + { + using var assetRequest = new HttpRequestMessage(HttpMethod.Get, assetUrl); - foreach (var folderPath in Directory.GetDirectories(source)) + foreach (var entry in headers) { - var folderName = Path.GetFileName(folderPath); - - Directory.CreateDirectory(Path.Combine(target, folderName)); - CloneFolder(folderPath, Path.Combine(target, folderName)); + assetRequest.Headers.Add(entry.Key, entry.Value); } - foreach (var file in Directory.GetFiles(source)) - { - File.Copy(file, Path.Combine(target, Path.GetFileName(file))); - } + var assetResponse = await _httpClient + .SendAsync(assetRequest, HttpCompletionOption.ResponseHeadersRead); + + assetResponse.EnsureSuccessStatusCode(); + + return assetResponse; } - private static async Task DownloadAndExtractAsync( - string assetName, - string assetUrl, - string targetPath, - Dictionary headers) + // download and extract + if (assetName.EndsWith(".zip", StringComparison.OrdinalIgnoreCase)) { - // get download stream - async Task GetAssetResponseAsync() - { - using var assetRequest = new HttpRequestMessage(HttpMethod.Get, assetUrl); + using var assetResponse = await GetAssetResponseAsync(); + using var stream = await assetResponse.Content.ReadAsStreamAsync(); + using var zipArchive = new ZipArchive(stream, ZipArchiveMode.Read); + zipArchive.ExtractToDirectory(targetPath); + } + else if (assetName.EndsWith(".tar.gz", StringComparison.OrdinalIgnoreCase)) + { + using var assetResponse = await GetAssetResponseAsync(); + using var stream = await assetResponse.Content.ReadAsStreamAsync(); + using var gzipStream = new GZipInputStream(stream); + using var tarArchive = TarArchive.CreateInputTarArchive(gzipStream, Encoding.UTF8); + tarArchive.ExtractContents(targetPath); + } + else + { + throw new Exception("Only assets of type .zip or .tar.gz are supported."); + } + } - foreach (var entry in headers) - { - assetRequest.Headers.Add(entry.Key, entry.Value); - } + #endregion - var assetResponse = await _httpClient - .SendAsync(assetRequest, HttpCompletionOption.ResponseHeadersRead); + #region local - assetResponse.EnsureSuccessStatusCode(); + private Task DiscoverLocalAsync(CancellationToken cancellationToken) + { + var rawResult = new List(); + var configuration = PackageReference.Configuration; - return assetResponse; - } + if (!configuration.TryGetValue("path", out var path)) + throw new ArgumentException("The 'path' parameter is missing in the extension reference."); - // download and extract - if (assetName.EndsWith(".zip", StringComparison.OrdinalIgnoreCase)) - { - using var assetResponse = await GetAssetResponseAsync(); - using var stream = await assetResponse.Content.ReadAsStreamAsync(); - using var zipArchive = new ZipArchive(stream, ZipArchiveMode.Read); - zipArchive.ExtractToDirectory(targetPath); - } - else if (assetName.EndsWith(".tar.gz", StringComparison.OrdinalIgnoreCase)) - { - using var assetResponse = await GetAssetResponseAsync(); - using var stream = await assetResponse.Content.ReadAsStreamAsync(); - using var gzipStream = new GZipInputStream(stream); - using var tarArchive = TarArchive.CreateInputTarArchive(gzipStream, Encoding.UTF8); - tarArchive.ExtractContents(targetPath); - } - else - { - throw new Exception("Only assets of type .zip or .tar.gz are supported."); - } + if (!Directory.Exists(path)) + throw new DirectoryNotFoundException($"The extension path {path} does not exist."); + + foreach (var folderPath in Directory.EnumerateDirectories(path)) + { + cancellationToken.ThrowIfCancellationRequested(); + + var folderName = Path.GetFileName(folderPath); + rawResult.Add(folderName); + _logger.LogDebug("Discovered package version {PackageVersion}", folderName); } - #endregion + var result = rawResult.OrderBy(value => value).Reverse(); - #region local + return Task.FromResult(result.ToArray()); + } - private Task DiscoverLocalAsync(CancellationToken cancellationToken) + private Task RestoreLocalAsync(string restoreRoot, CancellationToken cancellationToken) + { + return Task.Run(() => { - var rawResult = new List(); var configuration = PackageReference.Configuration; if (!configuration.TryGetValue("path", out var path)) throw new ArgumentException("The 'path' parameter is missing in the extension reference."); - if (!Directory.Exists(path)) - throw new DirectoryNotFoundException($"The extension path {path} does not exist."); - - foreach (var folderPath in Directory.EnumerateDirectories(path)) - { - cancellationToken.ThrowIfCancellationRequested(); + if (!configuration.TryGetValue("version", out var version)) + throw new ArgumentException("The 'version' parameter is missing in the extension reference."); - var folderName = Path.GetFileName(folderPath); - rawResult.Add(folderName); - _logger.LogDebug("Discovered package version {PackageVersion}", folderName); - } + var sourcePath = Path.Combine(path, version); - var result = rawResult.OrderBy(value => value).Reverse(); + if (!Directory.Exists(sourcePath)) + throw new DirectoryNotFoundException($"The source path {sourcePath} does not exist."); - return Task.FromResult(result.ToArray()); - } + var pathHash = new Guid(path.Hash()).ToString(); + var targetPath = Path.Combine(restoreRoot, pathHash, version); - private Task RestoreLocalAsync(string restoreRoot, CancellationToken cancellationToken) - { - return Task.Run(() => + if (!Directory.Exists(targetPath) || !Directory.EnumerateFileSystemEntries(targetPath).Any()) { - var configuration = PackageReference.Configuration; - - if (!configuration.TryGetValue("path", out var path)) - throw new ArgumentException("The 'path' parameter is missing in the extension reference."); - - if (!configuration.TryGetValue("version", out var version)) - throw new ArgumentException("The 'version' parameter is missing in the extension reference."); - - var sourcePath = Path.Combine(path, version); + _logger.LogDebug("Restore package from source {Source} to {Target}", sourcePath, targetPath); + CloneFolder(sourcePath, targetPath); + } + else + { + _logger.LogDebug("Package is already restored"); + } - if (!Directory.Exists(sourcePath)) - throw new DirectoryNotFoundException($"The source path {sourcePath} does not exist."); + return targetPath; + }, cancellationToken); + } - var pathHash = new Guid(path.Hash()).ToString(); - var targetPath = Path.Combine(restoreRoot, pathHash, version); + #endregion - if (!Directory.Exists(targetPath) || !Directory.EnumerateFileSystemEntries(targetPath).Any()) - { - _logger.LogDebug("Restore package from source {Source} to {Target}", sourcePath, targetPath); - CloneFolder(sourcePath, targetPath); - } - else - { - _logger.LogDebug("Package is already restored"); - } + #region git-tag - return targetPath; - }, cancellationToken); - } + private async Task DiscoverGitTagsAsync(CancellationToken cancellationToken) + { + const string refPrefix = "refs/tags/"; - #endregion + var result = new List(); + var configuration = PackageReference.Configuration; - #region git-tag + if (!configuration.TryGetValue("repository", out var repository)) + throw new ArgumentException("The 'repository' parameter is missing in the extension reference."); - private async Task DiscoverGitTagsAsync(CancellationToken cancellationToken) + var startInfo = new ProcessStartInfo { - const string refPrefix = "refs/tags/"; - - var result = new List(); - var configuration = PackageReference.Configuration; + CreateNoWindow = true, + FileName = "git", + Arguments = $"ls-remote --tags --sort=v:refname --refs {repository}", + RedirectStandardOutput = true, + RedirectStandardError = true + }; - if (!configuration.TryGetValue("repository", out var repository)) - throw new ArgumentException("The 'repository' parameter is missing in the extension reference."); + using var process = Process.Start(startInfo); - var startInfo = new ProcessStartInfo - { - CreateNoWindow = true, - FileName = "git", - Arguments = $"ls-remote --tags --sort=v:refname --refs {repository}", - RedirectStandardOutput = true, - RedirectStandardError = true - }; - - using var process = Process.Start(startInfo); + if (process is null) + throw new Exception("Process is null."); - if (process is null) - throw new Exception("Process is null."); + while (!process.StandardOutput.EndOfStream) + { + var refLine = await process.StandardOutput.ReadLineAsync(cancellationToken); - while (!process.StandardOutput.EndOfStream) + try { - var refLine = await process.StandardOutput.ReadLineAsync(cancellationToken); + var refString = refLine!.Split('\t')[1]; - try + if (refString.StartsWith(refPrefix)) { - var refString = refLine!.Split('\t')[1]; - - if (refString.StartsWith(refPrefix)) - { - var tag = refString[refPrefix.Length..]; - result.Add(tag); - } - - else - { - _logger.LogDebug("Unable to extract tag from ref {Ref}", refLine); - } + var tag = refString[refPrefix.Length..]; + result.Add(tag); } - catch + + else { _logger.LogDebug("Unable to extract tag from ref {Ref}", refLine); } } - - await process.WaitForExitAsync(cancellationToken); - - if (process.ExitCode != 0) + catch { - var escapedUriWithoutUserInfo = new Uri(repository) - .GetComponents(UriComponents.AbsoluteUri & ~UriComponents.UserInfo, UriFormat.UriEscaped); + _logger.LogDebug("Unable to extract tag from ref {Ref}", refLine); + } + } - var error = process is null - ? default : - $" Reason: {await process.StandardError.ReadToEndAsync(cancellationToken)}"; + await process.WaitForExitAsync(cancellationToken); - throw new Exception($"Unable to discover tags for repository {escapedUriWithoutUserInfo}.{error}"); - } + if (process.ExitCode != 0) + { + var escapedUriWithoutUserInfo = new Uri(repository) + .GetComponents(UriComponents.AbsoluteUri & ~UriComponents.UserInfo, UriFormat.UriEscaped); - result.Reverse(); + var error = process is null + ? default : + $" Reason: {await process.StandardError.ReadToEndAsync(cancellationToken)}"; - return result.ToArray(); + throw new Exception($"Unable to discover tags for repository {escapedUriWithoutUserInfo}.{error}"); } - private async Task RestoreGitTagAsync(string restoreRoot, CancellationToken cancellationToken) - { - var configuration = PackageReference.Configuration; + result.Reverse(); - if (!configuration.TryGetValue("repository", out var repository)) - throw new ArgumentException("The 'repository' parameter is missing in the extension reference."); + return result.ToArray(); + } - if (!configuration.TryGetValue("tag", out var tag)) - throw new ArgumentException("The 'tag' parameter is missing in the extension reference."); + private async Task RestoreGitTagAsync(string restoreRoot, CancellationToken cancellationToken) + { + var configuration = PackageReference.Configuration; - if (!configuration.TryGetValue("csproj", out var csproj)) - throw new ArgumentException("The 'csproj' parameter is missing in the extension reference."); + if (!configuration.TryGetValue("repository", out var repository)) + throw new ArgumentException("The 'repository' parameter is missing in the extension reference."); - var escapedUriWithoutSchemeAndUserInfo = new Uri(repository) - .GetComponents(UriComponents.AbsoluteUri & ~UriComponents.Scheme & ~UriComponents.UserInfo, UriFormat.UriEscaped); + if (!configuration.TryGetValue("tag", out var tag)) + throw new ArgumentException("The 'tag' parameter is missing in the extension reference."); - var targetPath = Path.Combine(restoreRoot, escapedUriWithoutSchemeAndUserInfo.Replace('/', '_').ToLower(), tag); + if (!configuration.TryGetValue("csproj", out var csproj)) + throw new ArgumentException("The 'csproj' parameter is missing in the extension reference."); - if (!Directory.Exists(targetPath) || !Directory.EnumerateFileSystemEntries(targetPath).Any()) + var escapedUriWithoutSchemeAndUserInfo = new Uri(repository) + .GetComponents(UriComponents.AbsoluteUri & ~UriComponents.Scheme & ~UriComponents.UserInfo, UriFormat.UriEscaped); + + var targetPath = Path.Combine(restoreRoot, escapedUriWithoutSchemeAndUserInfo.Replace('/', '_').ToLower(), tag); + + if (!Directory.Exists(targetPath) || !Directory.EnumerateFileSystemEntries(targetPath).Any()) + { + var cloneFolder = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + var publishFolder = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + + try { - var cloneFolder = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); - var publishFolder = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + // Clone respository + Directory.CreateDirectory(cloneFolder); - try + var startInfo1 = new ProcessStartInfo { - // Clone respository - Directory.CreateDirectory(cloneFolder); + CreateNoWindow = true, + FileName = "git", + Arguments = $"clone --depth 1 --branch {tag} --recurse-submodules {repository} {cloneFolder}", + RedirectStandardError = true + }; - var startInfo1 = new ProcessStartInfo - { - CreateNoWindow = true, - FileName = "git", - Arguments = $"clone --depth 1 --branch {tag} --recurse-submodules {repository} {cloneFolder}", - RedirectStandardError = true - }; + using var process1 = Process.Start(startInfo1); - using var process1 = Process.Start(startInfo1); + if (process1 is null) + throw new Exception("Process is null."); - if (process1 is null) - throw new Exception("Process is null."); + await process1.WaitForExitAsync(cancellationToken); - await process1.WaitForExitAsync(cancellationToken); - - if (process1.ExitCode != 0) - { - var escapedUriWithoutUserInfo = new Uri(repository) - .GetComponents(UriComponents.AbsoluteUri & ~UriComponents.UserInfo, UriFormat.UriEscaped); + if (process1.ExitCode != 0) + { + var escapedUriWithoutUserInfo = new Uri(repository) + .GetComponents(UriComponents.AbsoluteUri & ~UriComponents.UserInfo, UriFormat.UriEscaped); - var error = process1 is null - ? default : - $" Reason: {await process1.StandardError.ReadToEndAsync(cancellationToken)}"; + var error = process1 is null + ? default : + $" Reason: {await process1.StandardError.ReadToEndAsync(cancellationToken)}"; - throw new Exception($"Unable to clone repository {escapedUriWithoutUserInfo}.{error}"); - } + throw new Exception($"Unable to clone repository {escapedUriWithoutUserInfo}.{error}"); + } - // Publish project - var sourceFilePath = Path.Combine(cloneFolder, csproj); + // Publish project + var sourceFilePath = Path.Combine(cloneFolder, csproj); - if (!File.Exists(sourceFilePath)) - throw new Exception($"The .csproj file {csproj} does not exist."); + if (!File.Exists(sourceFilePath)) + throw new Exception($"The .csproj file {csproj} does not exist."); - Directory.CreateDirectory(targetPath); + Directory.CreateDirectory(targetPath); - var startInfo2 = new ProcessStartInfo - { - CreateNoWindow = true, - FileName = "dotnet", - Arguments = $"publish {sourceFilePath} -c Release -o {publishFolder}", - RedirectStandardError = true - }; + var startInfo2 = new ProcessStartInfo + { + CreateNoWindow = true, + FileName = "dotnet", + Arguments = $"publish {sourceFilePath} -c Release -o {publishFolder}", + RedirectStandardError = true + }; - using var process2 = Process.Start(startInfo2); + using var process2 = Process.Start(startInfo2); - if (process2 is null) - throw new Exception("Process is null."); + if (process2 is null) + throw new Exception("Process is null."); - await process2.WaitForExitAsync(cancellationToken); + await process2.WaitForExitAsync(cancellationToken); - if (process2.ExitCode != 0) - { - var escapedUriWithoutUserInfo = new Uri(repository) - .GetComponents(UriComponents.AbsoluteUri & ~UriComponents.UserInfo, UriFormat.UriEscaped); + if (process2.ExitCode != 0) + { + var escapedUriWithoutUserInfo = new Uri(repository) + .GetComponents(UriComponents.AbsoluteUri & ~UriComponents.UserInfo, UriFormat.UriEscaped); - var error = process2 is null - ? default : - $" Reason: {await process2.StandardError.ReadToEndAsync(cancellationToken)}"; + var error = process2 is null + ? default : + $" Reason: {await process2.StandardError.ReadToEndAsync(cancellationToken)}"; - throw new Exception($"Unable to publish project.{error}"); - } + throw new Exception($"Unable to publish project.{error}"); + } - // Clone folder - CloneFolder(publishFolder, targetPath); + // Clone folder + CloneFolder(publishFolder, targetPath); + } + catch + { + // try delete restore folder + try + { + if (Directory.Exists(targetPath)) + Directory.Delete(targetPath, recursive: true); } - catch + catch { } + + throw; + } + finally + { + // try delete clone folder + try { - // try delete restore folder - try - { - if (Directory.Exists(targetPath)) - Directory.Delete(targetPath, recursive: true); - } - catch { } - - throw; + if (Directory.Exists(cloneFolder)) + Directory.Delete(cloneFolder, recursive: true); } - finally + catch { } + + // try delete publish folder + try { - // try delete clone folder - try - { - if (Directory.Exists(cloneFolder)) - Directory.Delete(cloneFolder, recursive: true); - } - catch { } - - // try delete publish folder - try - { - if (Directory.Exists(publishFolder)) - Directory.Delete(publishFolder, recursive: true); - } - catch { } + if (Directory.Exists(publishFolder)) + Directory.Delete(publishFolder, recursive: true); } + catch { } } - else - { - _logger.LogDebug("Package is already restored"); - } - - return targetPath; + } + else + { + _logger.LogDebug("Package is already restored"); } - #endregion + return targetPath; + } - #region github-releases + #endregion - private async Task DiscoverGithubReleasesAsync(CancellationToken cancellationToken) - { - var result = new List(); - var configuration = PackageReference.Configuration; + #region github-releases - if (!configuration.TryGetValue("project-path", out var projectPath)) - throw new ArgumentException("The 'project-path' parameter is missing in the extension reference."); + private async Task DiscoverGithubReleasesAsync(CancellationToken cancellationToken) + { + var result = new List(); + var configuration = PackageReference.Configuration; - var server = $"https://api.github.com"; - var requestUrl = $"{server}/repos/{projectPath}/releases?per_page={PER_PAGE}&page={1}"; + if (!configuration.TryGetValue("project-path", out var projectPath)) + throw new ArgumentException("The 'project-path' parameter is missing in the extension reference."); - for (int i = 0; i < MAX_PAGES; i++) - { - using var request = new HttpRequestMessage(HttpMethod.Get, requestUrl); + var server = $"https://api.github.com"; + var requestUrl = $"{server}/repos/{projectPath}/releases?per_page={PER_PAGE}&page={1}"; - if (configuration.TryGetValue("token", out var token)) - request.Headers.Add("Authorization", $"token {token}"); + for (int i = 0; i < MAX_PAGES; i++) + { + using var request = new HttpRequestMessage(HttpMethod.Get, requestUrl); - request.Headers.Add("User-Agent", "Nexus"); - request.Headers.Add("Accept", "application/vnd.github.v3+json"); + if (configuration.TryGetValue("token", out var token)) + request.Headers.Add("Authorization", $"token {token}"); - using var response = await _httpClient.SendAsync(request, cancellationToken); - response.EnsureSuccessStatusCode(); + request.Headers.Add("User-Agent", "Nexus"); + request.Headers.Add("Accept", "application/vnd.github.v3+json"); - var contentStream = await response.Content.ReadAsStreamAsync(cancellationToken); - var jsonDocument = await JsonDocument.ParseAsync(contentStream, cancellationToken: cancellationToken); + using var response = await _httpClient.SendAsync(request, cancellationToken); + response.EnsureSuccessStatusCode(); - foreach (var githubRelease in jsonDocument.RootElement.EnumerateArray()) - { - var releaseTagName = githubRelease.GetProperty("tag_name").GetString() ?? throw new Exception("tag_name is null"); - result.Add(releaseTagName); - _logger.LogDebug("Discovered package version {PackageVersion}", releaseTagName); - } + var contentStream = await response.Content.ReadAsStreamAsync(cancellationToken); + var jsonDocument = await JsonDocument.ParseAsync(contentStream, cancellationToken: cancellationToken); - // look for more pages - response.Headers.TryGetValues("Link", out var links); + foreach (var githubRelease in jsonDocument.RootElement.EnumerateArray()) + { + var releaseTagName = githubRelease.GetProperty("tag_name").GetString() ?? throw new Exception("tag_name is null"); + result.Add(releaseTagName); + _logger.LogDebug("Discovered package version {PackageVersion}", releaseTagName); + } - if (links is null || !links.Any()) - break; + // look for more pages + response.Headers.TryGetValues("Link", out var links); - requestUrl = links - .First() - .Split(",") - .Where(current => current.Contains("rel=\"next\"")) - .Select(current => GitHubRegex().Match(current).Groups[1].Value) - .FirstOrDefault(); + if (links is null || !links.Any()) + break; - if (requestUrl == default) - break; + requestUrl = links + .First() + .Split(",") + .Where(current => current.Contains("rel=\"next\"")) + .Select(current => GitHubRegex().Match(current).Groups[1].Value) + .FirstOrDefault(); - continue; - } + if (requestUrl == default) + break; - return result.ToArray(); + continue; } - private async Task RestoreGitHubReleasesAsync(string restoreRoot, CancellationToken cancellationToken) - { - var configuration = PackageReference.Configuration; + return result.ToArray(); + } - if (!configuration.TryGetValue("project-path", out var projectPath)) - throw new ArgumentException("The 'project-path' parameter is missing in the extension reference."); + private async Task RestoreGitHubReleasesAsync(string restoreRoot, CancellationToken cancellationToken) + { + var configuration = PackageReference.Configuration; - if (!configuration.TryGetValue("tag", out var tag)) - throw new ArgumentException("The 'tag' parameter is missing in the extension reference."); + if (!configuration.TryGetValue("project-path", out var projectPath)) + throw new ArgumentException("The 'project-path' parameter is missing in the extension reference."); - if (!configuration.TryGetValue("asset-selector", out var assetSelector)) - throw new ArgumentException("The 'asset-selector' parameter is missing in the extension reference."); + if (!configuration.TryGetValue("tag", out var tag)) + throw new ArgumentException("The 'tag' parameter is missing in the extension reference."); - var targetPath = Path.Combine(restoreRoot, projectPath.Replace('/', '_').ToLower(), tag); + if (!configuration.TryGetValue("asset-selector", out var assetSelector)) + throw new ArgumentException("The 'asset-selector' parameter is missing in the extension reference."); - if (!Directory.Exists(targetPath) || !Directory.EnumerateFileSystemEntries(targetPath).Any()) - { - var server = $"https://api.github.com"; - var requestUrl = $"{server}/repos/{projectPath}/releases/tags/{tag}"; + var targetPath = Path.Combine(restoreRoot, projectPath.Replace('/', '_').ToLower(), tag); - using var request = new HttpRequestMessage(HttpMethod.Get, requestUrl); + if (!Directory.Exists(targetPath) || !Directory.EnumerateFileSystemEntries(targetPath).Any()) + { + var server = $"https://api.github.com"; + var requestUrl = $"{server}/repos/{projectPath}/releases/tags/{tag}"; - if (configuration.TryGetValue("token", out var token)) - request.Headers.Add("Authorization", $"token {token}"); + using var request = new HttpRequestMessage(HttpMethod.Get, requestUrl); - request.Headers.Add("User-Agent", "Nexus"); - request.Headers.Add("Accept", "application/vnd.github.v3+json"); + if (configuration.TryGetValue("token", out var token)) + request.Headers.Add("Authorization", $"token {token}"); - using var response = await _httpClient.SendAsync(request, cancellationToken); - response.EnsureSuccessStatusCode(); + request.Headers.Add("User-Agent", "Nexus"); + request.Headers.Add("Accept", "application/vnd.github.v3+json"); - var contentStream = await response.Content.ReadAsStreamAsync(cancellationToken); - var jsonDocument = await JsonDocument.ParseAsync(contentStream, cancellationToken: cancellationToken); + using var response = await _httpClient.SendAsync(request, cancellationToken); + response.EnsureSuccessStatusCode(); - // find asset - var gitHubRelease = jsonDocument.RootElement; - var releaseTagName = gitHubRelease.GetProperty("tag_name").GetString(); + var contentStream = await response.Content.ReadAsStreamAsync(cancellationToken); + var jsonDocument = await JsonDocument.ParseAsync(contentStream, cancellationToken: cancellationToken); - var asset = gitHubRelease - .GetProperty("assets") - .EnumerateArray() - .FirstOrDefault(current => Regex.IsMatch(current.GetProperty("name").GetString() ?? throw new Exception("assets is null"), assetSelector)); + // find asset + var gitHubRelease = jsonDocument.RootElement; + var releaseTagName = gitHubRelease.GetProperty("tag_name").GetString(); - if (asset.ValueKind != JsonValueKind.Undefined) - { - // get asset download URL - var assetUrl = asset.GetProperty("url").GetString() ?? throw new Exception("url is null"); - var assetBrowserUrl = asset.GetProperty("browser_download_url").GetString() ?? throw new Exception("browser_download_url is null"); + var asset = gitHubRelease + .GetProperty("assets") + .EnumerateArray() + .FirstOrDefault(current => Regex.IsMatch(current.GetProperty("name").GetString() ?? throw new Exception("assets is null"), assetSelector)); - // get download stream - var headers = new Dictionary(); + if (asset.ValueKind != JsonValueKind.Undefined) + { + // get asset download URL + var assetUrl = asset.GetProperty("url").GetString() ?? throw new Exception("url is null"); + var assetBrowserUrl = asset.GetProperty("browser_download_url").GetString() ?? throw new Exception("browser_download_url is null"); - if (configuration.TryGetValue("token", out var assetToken)) - headers["Authorization"] = $"token {assetToken}"; + // get download stream + var headers = new Dictionary(); - headers["User-Agent"] = "Nexus"; - headers["Accept"] = "application/octet-stream"; + if (configuration.TryGetValue("token", out var assetToken)) + headers["Authorization"] = $"token {assetToken}"; - _logger.LogDebug("Restore package from source {Source} to {Target}", assetBrowserUrl, targetPath); - await DownloadAndExtractAsync(assetBrowserUrl, assetUrl, targetPath, headers); - } - else - { - throw new Exception("No matching assets found."); - } + headers["User-Agent"] = "Nexus"; + headers["Accept"] = "application/octet-stream"; + + _logger.LogDebug("Restore package from source {Source} to {Target}", assetBrowserUrl, targetPath); + await DownloadAndExtractAsync(assetBrowserUrl, assetUrl, targetPath, headers); } else { - _logger.LogDebug("Package is already restored"); + throw new Exception("No matching assets found."); } - - return targetPath; + } + else + { + _logger.LogDebug("Package is already restored"); } - #endregion + return targetPath; + } - #region gitlab-packages-generic-v4 + #endregion - // curl --header "PRIVATE-TOKEN: N1umphXDULLvgzhT7uyx" \ - // --upload-file assets.tar.gz \ - // "https:///api/v4/projects//packages/generic///assets.tar.gz" + #region gitlab-packages-generic-v4 - private async Task DiscoverGitLabPackagesGenericAsync(CancellationToken cancellationToken) - { - var result = new List(); - var configuration = PackageReference.Configuration; - - if (!configuration.TryGetValue("server", out var server)) - throw new ArgumentException("The 'server' parameter is missing in the extension reference."); + // curl --header "PRIVATE-TOKEN: N1umphXDULLvgzhT7uyx" \ + // --upload-file assets.tar.gz \ + // "https:///api/v4/projects//packages/generic///assets.tar.gz" - if (!configuration.TryGetValue("project-path", out var projectPath)) - throw new ArgumentException("The 'project-path' parameter is missing in the extension reference."); + private async Task DiscoverGitLabPackagesGenericAsync(CancellationToken cancellationToken) + { + var result = new List(); + var configuration = PackageReference.Configuration; - if (!configuration.TryGetValue("package", out var package)) - throw new ArgumentException("The 'package' parameter is missing in the extension reference."); + if (!configuration.TryGetValue("server", out var server)) + throw new ArgumentException("The 'server' parameter is missing in the extension reference."); - configuration.TryGetValue("token", out var token); + if (!configuration.TryGetValue("project-path", out var projectPath)) + throw new ArgumentException("The 'project-path' parameter is missing in the extension reference."); - var headers = new Dictionary(); + if (!configuration.TryGetValue("package", out var package)) + throw new ArgumentException("The 'package' parameter is missing in the extension reference."); - if (!string.IsNullOrWhiteSpace(token)) - headers["PRIVATE-TOKEN"] = token; + configuration.TryGetValue("token", out var token); - await foreach (var gitlabPackage in GetGitLabPackagesGenericAsync(server, projectPath, package, headers, cancellationToken)) - { - var packageVersion = gitlabPackage.GetProperty("version").GetString() ?? throw new Exception("version is null"); - result.Add(packageVersion); - _logger.LogDebug("Discovered package version {PackageVersion}", packageVersion); - } + var headers = new Dictionary(); - result.Reverse(); + if (!string.IsNullOrWhiteSpace(token)) + headers["PRIVATE-TOKEN"] = token; - return result.ToArray(); + await foreach (var gitlabPackage in GetGitLabPackagesGenericAsync(server, projectPath, package, headers, cancellationToken)) + { + var packageVersion = gitlabPackage.GetProperty("version").GetString() ?? throw new Exception("version is null"); + result.Add(packageVersion); + _logger.LogDebug("Discovered package version {PackageVersion}", packageVersion); } - private async Task RestoreGitLabPackagesGenericAsync(string restoreRoot, CancellationToken cancellationToken) - { - var configuration = PackageReference.Configuration; + result.Reverse(); - if (!configuration.TryGetValue("server", out var server)) - throw new ArgumentException("The 'server' parameter is missing in the extension reference."); + return result.ToArray(); + } - if (!configuration.TryGetValue("project-path", out var projectPath)) - throw new ArgumentException("The 'project-path' parameter is missing in the extension reference."); + private async Task RestoreGitLabPackagesGenericAsync(string restoreRoot, CancellationToken cancellationToken) + { + var configuration = PackageReference.Configuration; - if (!configuration.TryGetValue("package", out var package)) - throw new ArgumentException("The 'package' parameter is missing in the extension reference."); + if (!configuration.TryGetValue("server", out var server)) + throw new ArgumentException("The 'server' parameter is missing in the extension reference."); - if (!configuration.TryGetValue("version", out var version)) - throw new ArgumentException("The 'version' parameter is missing in the extension reference."); + if (!configuration.TryGetValue("project-path", out var projectPath)) + throw new ArgumentException("The 'project-path' parameter is missing in the extension reference."); - if (!configuration.TryGetValue("asset-selector", out var assetSelector)) - throw new ArgumentException("The 'asset-selector' parameter is missing in the extension reference."); + if (!configuration.TryGetValue("package", out var package)) + throw new ArgumentException("The 'package' parameter is missing in the extension reference."); - configuration.TryGetValue("token", out var token); + if (!configuration.TryGetValue("version", out var version)) + throw new ArgumentException("The 'version' parameter is missing in the extension reference."); - var targetPath = Path.Combine(restoreRoot, projectPath.Replace('/', '_').ToLower(), version); + if (!configuration.TryGetValue("asset-selector", out var assetSelector)) + throw new ArgumentException("The 'asset-selector' parameter is missing in the extension reference."); - if (!Directory.Exists(targetPath) || !Directory.EnumerateFileSystemEntries(targetPath).Any()) - { - var headers = new Dictionary(); + configuration.TryGetValue("token", out var token); - if (!string.IsNullOrWhiteSpace(token)) - headers["PRIVATE-TOKEN"] = token; + var targetPath = Path.Combine(restoreRoot, projectPath.Replace('/', '_').ToLower(), version); - // get package id - var gitlabPackage = default(JsonElement); + if (!Directory.Exists(targetPath) || !Directory.EnumerateFileSystemEntries(targetPath).Any()) + { + var headers = new Dictionary(); - await foreach (var currentPackage in - GetGitLabPackagesGenericAsync(server, projectPath, package, headers, cancellationToken)) - { - var packageVersion = currentPackage.GetProperty("version").GetString(); + if (!string.IsNullOrWhiteSpace(token)) + headers["PRIVATE-TOKEN"] = token; - if (packageVersion == version) - gitlabPackage = currentPackage; - } + // get package id + var gitlabPackage = default(JsonElement); - if (gitlabPackage.ValueKind == JsonValueKind.Undefined) - throw new Exception("The specified version could not be found."); + await foreach (var currentPackage in + GetGitLabPackagesGenericAsync(server, projectPath, package, headers, cancellationToken)) + { + var packageVersion = currentPackage.GetProperty("version").GetString(); - var packageId = gitlabPackage.GetProperty("id").GetInt32(); + if (packageVersion == version) + gitlabPackage = currentPackage; + } - // list package files (https://docs.gitlab.com/ee/api/packages.html#list-package-files) - var encodedProjectPath = WebUtility.UrlEncode(projectPath); - var requestUrl = $"{server}/api/v4/projects/{encodedProjectPath}/packages/{packageId}/package_files"; + if (gitlabPackage.ValueKind == JsonValueKind.Undefined) + throw new Exception("The specified version could not be found."); - using var request = new HttpRequestMessage(HttpMethod.Get, requestUrl); + var packageId = gitlabPackage.GetProperty("id").GetInt32(); - foreach (var entry in headers) - { - request.Headers.Add(entry.Key, entry.Value); - } + // list package files (https://docs.gitlab.com/ee/api/packages.html#list-package-files) + var encodedProjectPath = WebUtility.UrlEncode(projectPath); + var requestUrl = $"{server}/api/v4/projects/{encodedProjectPath}/packages/{packageId}/package_files"; - using var response = await _httpClient.SendAsync(request, cancellationToken); - response.EnsureSuccessStatusCode(); + using var request = new HttpRequestMessage(HttpMethod.Get, requestUrl); - var contentStream = await response.Content.ReadAsStreamAsync(cancellationToken); - var jsonDocument = await JsonDocument.ParseAsync(contentStream, cancellationToken: cancellationToken); + foreach (var entry in headers) + { + request.Headers.Add(entry.Key, entry.Value); + } - // find asset - var asset = jsonDocument.RootElement.EnumerateArray() - .FirstOrDefault(current => Regex.IsMatch(current.GetProperty("file_name").GetString() ?? throw new Exception("file_name is null"), assetSelector)); + using var response = await _httpClient.SendAsync(request, cancellationToken); + response.EnsureSuccessStatusCode(); - var fileName = asset.GetProperty("file_name").GetString() ?? throw new Exception("file_name is null"); + var contentStream = await response.Content.ReadAsStreamAsync(cancellationToken); + var jsonDocument = await JsonDocument.ParseAsync(contentStream, cancellationToken: cancellationToken); - if (asset.ValueKind != JsonValueKind.Undefined) - { - // download package file (https://docs.gitlab.com/ee/user/packages/generic_packages/index.html#download-package-file) - var assetUrl = $"{server}/api/v4/projects/{encodedProjectPath}/packages/generic/{package}/{version}/{fileName}"; - _logger.LogDebug("Restore package from source {Source} to {Target}", assetUrl, targetPath); - await DownloadAndExtractAsync(fileName, assetUrl, targetPath, headers); - } - else - { - throw new Exception("No matching assets found."); - } + // find asset + var asset = jsonDocument.RootElement.EnumerateArray() + .FirstOrDefault(current => Regex.IsMatch(current.GetProperty("file_name").GetString() ?? throw new Exception("file_name is null"), assetSelector)); + + var fileName = asset.GetProperty("file_name").GetString() ?? throw new Exception("file_name is null"); + + if (asset.ValueKind != JsonValueKind.Undefined) + { + // download package file (https://docs.gitlab.com/ee/user/packages/generic_packages/index.html#download-package-file) + var assetUrl = $"{server}/api/v4/projects/{encodedProjectPath}/packages/generic/{package}/{version}/{fileName}"; + _logger.LogDebug("Restore package from source {Source} to {Target}", assetUrl, targetPath); + await DownloadAndExtractAsync(fileName, assetUrl, targetPath, headers); } else { - _logger.LogDebug("Package is already restored"); + throw new Exception("No matching assets found."); } - - return targetPath; } - - private static async IAsyncEnumerable GetGitLabPackagesGenericAsync( - string server, string projectPath, string package, Dictionary headers, [EnumeratorCancellation] CancellationToken cancellationToken) + else { - // list packages (https://docs.gitlab.com/ee/api/packages.html#within-a-project) - var encodedProjectPath = WebUtility.UrlEncode(projectPath); - var requestUrl = $"{server}/api/v4/projects/{encodedProjectPath}/packages?package_type=generic&package_name={package}&per_page={PER_PAGE}&page={1}"; + _logger.LogDebug("Package is already restored"); + } - for (int i = 0; i < MAX_PAGES; i++) - { - cancellationToken.ThrowIfCancellationRequested(); + return targetPath; + } - using var request = new HttpRequestMessage(HttpMethod.Get, requestUrl); + private static async IAsyncEnumerable GetGitLabPackagesGenericAsync( + string server, string projectPath, string package, Dictionary headers, [EnumeratorCancellation] CancellationToken cancellationToken) + { + // list packages (https://docs.gitlab.com/ee/api/packages.html#within-a-project) + var encodedProjectPath = WebUtility.UrlEncode(projectPath); + var requestUrl = $"{server}/api/v4/projects/{encodedProjectPath}/packages?package_type=generic&package_name={package}&per_page={PER_PAGE}&page={1}"; - foreach (var entry in headers) - { - request.Headers.Add(entry.Key, entry.Value); - } + for (int i = 0; i < MAX_PAGES; i++) + { + cancellationToken.ThrowIfCancellationRequested(); - using var response = await _httpClient.SendAsync(request, cancellationToken); - response.EnsureSuccessStatusCode(); + using var request = new HttpRequestMessage(HttpMethod.Get, requestUrl); - var message = await response.Content.ReadAsStringAsync(cancellationToken); + foreach (var entry in headers) + { + request.Headers.Add(entry.Key, entry.Value); + } - var contentStream = await response.Content.ReadAsStreamAsync(cancellationToken); - var jsonDocument = await JsonDocument.ParseAsync(contentStream, cancellationToken: cancellationToken); + using var response = await _httpClient.SendAsync(request, cancellationToken); + response.EnsureSuccessStatusCode(); - foreach (var gitlabPackage in jsonDocument.RootElement.EnumerateArray()) - { - yield return gitlabPackage; - } + var message = await response.Content.ReadAsStringAsync(cancellationToken); - // look for more pages - response.Headers.TryGetValues("Link", out var links); + var contentStream = await response.Content.ReadAsStreamAsync(cancellationToken); + var jsonDocument = await JsonDocument.ParseAsync(contentStream, cancellationToken: cancellationToken); - if (links is null) - throw new Exception("link is null"); + foreach (var gitlabPackage in jsonDocument.RootElement.EnumerateArray()) + { + yield return gitlabPackage; + } - if (!links.Any()) - break; + // look for more pages + response.Headers.TryGetValues("Link", out var links); - requestUrl = links - .First() - .Split(",") - .Where(current => current.Contains("rel=\"next\"")) - .Select(current => GitlabRegex().Match(current).Groups[1].Value) - .FirstOrDefault(); + if (links is null) + throw new Exception("link is null"); - if (requestUrl == default) - break; + if (!links.Any()) + break; - continue; - } + requestUrl = links + .First() + .Split(",") + .Where(current => current.Contains("rel=\"next\"")) + .Select(current => GitlabRegex().Match(current).Groups[1].Value) + .FirstOrDefault(); + + if (requestUrl == default) + break; + + continue; } + } - [GeneratedRegex("\\<(https:.*)\\>; rel=\"next\"")] - private static partial Regex GitHubRegex(); + [GeneratedRegex("\\<(https:.*)\\>; rel=\"next\"")] + private static partial Regex GitHubRegex(); - [GeneratedRegex("\\<(https:.*)\\>; rel=\"next\"")] - private static partial Regex GitlabRegex(); + [GeneratedRegex("\\<(https:.*)\\>; rel=\"next\"")] + private static partial Regex GitlabRegex(); - #endregion + #endregion - #region gitlab-releases-v4 + #region gitlab-releases-v4 - /* The GitLab Releases approach does work until trying to download the previously uploaded file. - * GitLab allows only cookie-based downloads, tokens are not supported. Probably to stop the - * exact intentation to download data in an automated way. */ + /* The GitLab Releases approach does work until trying to download the previously uploaded file. + * GitLab allows only cookie-based downloads, tokens are not supported. Probably to stop the + * exact intentation to download data in an automated way. */ - //private async Task> DiscoverGitLabReleasesAsync(CancellationToken cancellationToken) - //{ - // var result = new Dictionary(); + //private async Task> DiscoverGitLabReleasesAsync(CancellationToken cancellationToken) + //{ + // var result = new Dictionary(); - // if (!_packageReference.TryGetValue("server", out var server)) - // throw new ArgumentException("The 'server' parameter is missing in the extension reference."); + // if (!_packageReference.TryGetValue("server", out var server)) + // throw new ArgumentException("The 'server' parameter is missing in the extension reference."); - // if (!_packageReference.TryGetValue("project-path", out var projectPath)) - // throw new ArgumentException("The 'project-path' parameter is missing in the extension reference."); + // if (!_packageReference.TryGetValue("project-path", out var projectPath)) + // throw new ArgumentException("The 'project-path' parameter is missing in the extension reference."); - // var encodedProjectPath = WebUtility.UrlEncode(projectPath); - // var requestUrl = $"{server}/api/v4/projects/{encodedProjectPath}/releases?per_page={PER_PAGE}&page={1}"; + // var encodedProjectPath = WebUtility.UrlEncode(projectPath); + // var requestUrl = $"{server}/api/v4/projects/{encodedProjectPath}/releases?per_page={PER_PAGE}&page={1}"; - // for (int i = 0; i < MAX_PAGES; i++) - // { - // using var request = new HttpRequestMessage(HttpMethod.Get, requestUrl); + // for (int i = 0; i < MAX_PAGES; i++) + // { + // using var request = new HttpRequestMessage(HttpMethod.Get, requestUrl); - // if (_packageReference.TryGetValue("token", out var token)) - // request.Headers.Add("PRIVATE-TOKEN", token); + // if (_packageReference.TryGetValue("token", out var token)) + // request.Headers.Add("PRIVATE-TOKEN", token); - // using var response = await _httpClient.SendAsync(request); - // response.EnsureSuccessStatusCode(); + // using var response = await _httpClient.SendAsync(request); + // response.EnsureSuccessStatusCode(); - // var contentStream = await response.Content.ReadAsStreamAsync(); - // var jsonDocument = await JsonDocument.ParseAsync(contentStream); + // var contentStream = await response.Content.ReadAsStreamAsync(); + // var jsonDocument = await JsonDocument.ParseAsync(contentStream); - // foreach (var gitlabRelease in jsonDocument.RootElement.EnumerateArray()) - // { - // var releaseTagName = gitlabRelease.GetProperty("tag_name").GetString(); + // foreach (var gitlabRelease in jsonDocument.RootElement.EnumerateArray()) + // { + // var releaseTagName = gitlabRelease.GetProperty("tag_name").GetString(); - // var isSemanticVersion = PackageLoadContext - // .TryParseWithPrefix(releaseTagName, out var semanticVersion); + // var isSemanticVersion = PackageLoadContext + // .TryParseWithPrefix(releaseTagName, out var semanticVersion); - // if (isSemanticVersion) - // result[semanticVersion] = releaseTagName; + // if (isSemanticVersion) + // result[semanticVersion] = releaseTagName; - // _logger.LogDebug("Discovered package version {PackageVersion}", releaseTagName); - // } + // _logger.LogDebug("Discovered package version {PackageVersion}", releaseTagName); + // } - // // look for more pages - // response.Headers.TryGetValues("Link", out var links); + // // look for more pages + // response.Headers.TryGetValues("Link", out var links); - // if (!links.Any()) - // break; + // if (!links.Any()) + // break; - // requestUrl = links - // .First() - // .Split(",") - // .Where(current => current.Contains("rel=\"next\"")) - // .Select(current => Regex.Match(current, @"\<(https:.*)\>; rel=""next""").Groups[1].Value) - // .FirstOrDefault(); + // requestUrl = links + // .First() + // .Split(",") + // .Where(current => current.Contains("rel=\"next\"")) + // .Select(current => Regex.Match(current, @"\<(https:.*)\>; rel=""next""").Groups[1].Value) + // .FirstOrDefault(); - // if (requestUrl == default) - // break; + // if (requestUrl == default) + // break; - // continue; - // } + // continue; + // } - // return result; - //} + // return result; + //} - //private async Task RestoreGitLabReleasesAsync(string restoreRoot, CancellationToken cancellationToken) - //{ - // if (!_packageReference.TryGetValue("server", out var server)) - // throw new ArgumentException("The 'server' parameter is missing in the extension reference."); + //private async Task RestoreGitLabReleasesAsync(string restoreRoot, CancellationToken cancellationToken) + //{ + // if (!_packageReference.TryGetValue("server", out var server)) + // throw new ArgumentException("The 'server' parameter is missing in the extension reference."); - // if (!_packageReference.TryGetValue("project-path", out var projectPath)) - // throw new ArgumentException("The 'ProjectPath' parameter is missing in the extension reference."); + // if (!_packageReference.TryGetValue("project-path", out var projectPath)) + // throw new ArgumentException("The 'ProjectPath' parameter is missing in the extension reference."); - // if (!_packageReference.TryGetValue("tag", out var tag)) - // throw new ArgumentException("The 'tag' parameter is missing in the extension reference."); + // if (!_packageReference.TryGetValue("tag", out var tag)) + // throw new ArgumentException("The 'tag' parameter is missing in the extension reference."); - // if (!_packageReference.TryGetValue("asset-selector", out var assetSelector)) - // throw new ArgumentException("The 'asset-selector' parameter is missing in the extension reference."); + // if (!_packageReference.TryGetValue("asset-selector", out var assetSelector)) + // throw new ArgumentException("The 'asset-selector' parameter is missing in the extension reference."); - // var targetPath = Path.Combine(restoreRoot, projectPath.Replace('/', '_').ToLower(), tag); + // var targetPath = Path.Combine(restoreRoot, projectPath.Replace('/', '_').ToLower(), tag); - // if (!Directory.Exists(targetPath) || Directory.EnumerateFileSystemEntries(targetPath).Any()) - // { - // var encodedProjectPath = WebUtility.UrlEncode(projectPath); - // var requestUrl = $"{server}/api/v4/projects/{encodedProjectPath}/releases/{tag}"; + // if (!Directory.Exists(targetPath) || Directory.EnumerateFileSystemEntries(targetPath).Any()) + // { + // var encodedProjectPath = WebUtility.UrlEncode(projectPath); + // var requestUrl = $"{server}/api/v4/projects/{encodedProjectPath}/releases/{tag}"; - // using var request = new HttpRequestMessage(HttpMethod.Get, requestUrl); + // using var request = new HttpRequestMessage(HttpMethod.Get, requestUrl); - // if (_packageReference.TryGetValue("token", out var token)) - // request.Headers.Add("PRIVATE-TOKEN", token); + // if (_packageReference.TryGetValue("token", out var token)) + // request.Headers.Add("PRIVATE-TOKEN", token); - // using var response = await _httpClient.SendAsync(request); - // response.EnsureSuccessStatusCode(); + // using var response = await _httpClient.SendAsync(request); + // response.EnsureSuccessStatusCode(); - // var contentStream = await response.Content.ReadAsStreamAsync(); - // var jsonDocument = await JsonDocument.ParseAsync(contentStream); + // var contentStream = await response.Content.ReadAsStreamAsync(); + // var jsonDocument = await JsonDocument.ParseAsync(contentStream); - // // find asset - // var gitHubRelease = jsonDocument.RootElement; - // var releaseTagName = gitHubRelease.GetProperty("tag_name").GetString(); + // // find asset + // var gitHubRelease = jsonDocument.RootElement; + // var releaseTagName = gitHubRelease.GetProperty("tag_name").GetString(); - // var isSemanticVersion = PackageLoadContext - // .TryParseWithPrefix(releaseTagName, out var semanticVersion); + // var isSemanticVersion = PackageLoadContext + // .TryParseWithPrefix(releaseTagName, out var semanticVersion); - // var asset = gitHubRelease - // .GetProperty("assets").GetProperty("links") - // .EnumerateArray() - // .FirstOrDefault(current => Regex.IsMatch(current.GetProperty("name").GetString(), assetSelector)); + // var asset = gitHubRelease + // .GetProperty("assets").GetProperty("links") + // .EnumerateArray() + // .FirstOrDefault(current => Regex.IsMatch(current.GetProperty("name").GetString(), assetSelector)); - // if (asset.ValueKind != JsonValueKind.Undefined) - // { - // var assetUrl = new Uri(asset.GetProperty("direct_asset_url").GetString()); - // _logger.LogDebug("Restore package from source {Source}", assetUrl); - // await DownloadAndExtractAsync(fileName, assetUrl, targetPath, headers, cancellationToken); - // } - // else - // { - // throw new Exception("No matching assets found."); - // } - // } - // else - // { - // _logger.LogDebug("Package is already restored"); - // } - // - // return targetPath; - //} + // if (asset.ValueKind != JsonValueKind.Undefined) + // { + // var assetUrl = new Uri(asset.GetProperty("direct_asset_url").GetString()); + // _logger.LogDebug("Restore package from source {Source}", assetUrl); + // await DownloadAndExtractAsync(fileName, assetUrl, targetPath, headers, cancellationToken); + // } + // else + // { + // throw new Exception("No matching assets found."); + // } + // } + // else + // { + // _logger.LogDebug("Package is already restored"); + // } + // + // return targetPath; + //} - #endregion + #endregion - } } diff --git a/src/Nexus/PackageManagement/PackageLoadContext.cs b/src/Nexus/PackageManagement/PackageLoadContext.cs index af5a8918..d7bbc746 100644 --- a/src/Nexus/PackageManagement/PackageLoadContext.cs +++ b/src/Nexus/PackageManagement/PackageLoadContext.cs @@ -1,47 +1,46 @@ using System.Reflection; using System.Runtime.Loader; -namespace Nexus.PackageManagement -{ - internal class PackageLoadContext : AssemblyLoadContext - { - #region Fields +namespace Nexus.PackageManagement; - private readonly AssemblyDependencyResolver _resolver; +internal class PackageLoadContext : AssemblyLoadContext +{ + #region Fields - #endregion + private readonly AssemblyDependencyResolver _resolver; - #region Constructors + #endregion - public PackageLoadContext(string entryDllPath) : base(isCollectible: true) - { - _resolver = new AssemblyDependencyResolver(entryDllPath); - } + #region Constructors - #endregion + public PackageLoadContext(string entryDllPath) : base(isCollectible: true) + { + _resolver = new AssemblyDependencyResolver(entryDllPath); + } - #region Methods + #endregion - protected override Assembly? Load(AssemblyName assemblyName) - { - var assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName); + #region Methods - if (assemblyPath is not null) - return LoadFromAssemblyPath(assemblyPath); + protected override Assembly? Load(AssemblyName assemblyName) + { + var assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName); - return null; - } + if (assemblyPath is not null) + return LoadFromAssemblyPath(assemblyPath); - protected override IntPtr LoadUnmanagedDll(string unmanagedDllName) - { - var libraryPath = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName); + return null; + } - if (libraryPath is not null) - return LoadUnmanagedDllFromPath(libraryPath); + protected override IntPtr LoadUnmanagedDll(string unmanagedDllName) + { + var libraryPath = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName); - return IntPtr.Zero; - } + if (libraryPath is not null) + return LoadUnmanagedDllFromPath(libraryPath); - #endregion + return IntPtr.Zero; } + + #endregion } diff --git a/src/Nexus/Services/AppStateManager.cs b/src/Nexus/Services/AppStateManager.cs index 0ffae093..17ac62d1 100644 --- a/src/Nexus/Services/AppStateManager.cs +++ b/src/Nexus/Services/AppStateManager.cs @@ -5,310 +5,309 @@ using System.Reflection; using System.Text.Json; -namespace Nexus.Services +namespace Nexus.Services; + +internal class AppStateManager { - internal class AppStateManager - { - #region Fields + #region Fields - private readonly IExtensionHive _extensionHive; - private readonly ICatalogManager _catalogManager; - private readonly IDatabaseService _databaseService; - private readonly ILogger _logger; - private readonly SemaphoreSlim _refreshDatabaseSemaphore = new(initialCount: 1, maxCount: 1); - private readonly SemaphoreSlim _projectSemaphore = new(initialCount: 1, maxCount: 1); + private readonly IExtensionHive _extensionHive; + private readonly ICatalogManager _catalogManager; + private readonly IDatabaseService _databaseService; + private readonly ILogger _logger; + private readonly SemaphoreSlim _refreshDatabaseSemaphore = new(initialCount: 1, maxCount: 1); + private readonly SemaphoreSlim _projectSemaphore = new(initialCount: 1, maxCount: 1); - #endregion + #endregion - #region Constructors + #region Constructors - public AppStateManager( - AppState appState, - IExtensionHive extensionHive, - ICatalogManager catalogManager, - IDatabaseService databaseService, - ILogger logger) - { - AppState = appState; - _extensionHive = extensionHive; - _catalogManager = catalogManager; - _databaseService = databaseService; - _logger = logger; - } + public AppStateManager( + AppState appState, + IExtensionHive extensionHive, + ICatalogManager catalogManager, + IDatabaseService databaseService, + ILogger logger) + { + AppState = appState; + _extensionHive = extensionHive; + _catalogManager = catalogManager; + _databaseService = databaseService; + _logger = logger; + } + + #endregion - #endregion + #region Properties - #region Properties + public AppState AppState { get; } - public AppState AppState { get; } + #endregion - #endregion + #region Methods - #region Methods + public async Task RefreshDatabaseAsync( + IProgress progress, + CancellationToken cancellationToken) + { + await _refreshDatabaseSemaphore.WaitAsync(cancellationToken); - public async Task RefreshDatabaseAsync( - IProgress progress, - CancellationToken cancellationToken) + try { - await _refreshDatabaseSemaphore.WaitAsync(cancellationToken); + // TODO: make atomic + var refreshDatabaseTask = AppState.ReloadPackagesTask; - try - { - // TODO: make atomic - var refreshDatabaseTask = AppState.ReloadPackagesTask; - - if (refreshDatabaseTask is null) - { - /* create fresh app state */ - AppState.CatalogState = new CatalogState( - Root: CatalogContainer.CreateRoot(_catalogManager, _databaseService), - Cache: new CatalogCache() - ); - - /* load packages */ - _logger.LogInformation("Load packages"); - - refreshDatabaseTask = _extensionHive - .LoadPackagesAsync(AppState.Project.PackageReferences.Values, progress, cancellationToken) - .ContinueWith(task => - { - LoadDataWriters(); - AppState.ReloadPackagesTask = default; - return Task.CompletedTask; - }, TaskScheduler.Default); - } - } - finally + if (refreshDatabaseTask is null) { - _refreshDatabaseSemaphore.Release(); + /* create fresh app state */ + AppState.CatalogState = new CatalogState( + Root: CatalogContainer.CreateRoot(_catalogManager, _databaseService), + Cache: new CatalogCache() + ); + + /* load packages */ + _logger.LogInformation("Load packages"); + + refreshDatabaseTask = _extensionHive + .LoadPackagesAsync(AppState.Project.PackageReferences.Values, progress, cancellationToken) + .ContinueWith(task => + { + LoadDataWriters(); + AppState.ReloadPackagesTask = default; + return Task.CompletedTask; + }, TaskScheduler.Default); } } - - public async Task PutPackageReferenceAsync( - InternalPackageReference packageReference) + finally { - await _projectSemaphore.WaitAsync(); - - try - { - var project = AppState.Project; + _refreshDatabaseSemaphore.Release(); + } + } - var newPackageReferences = project.PackageReferences - .ToDictionary(current => current.Key, current => current.Value); + public async Task PutPackageReferenceAsync( + InternalPackageReference packageReference) + { + await _projectSemaphore.WaitAsync(); - newPackageReferences[packageReference.Id] = packageReference; + try + { + var project = AppState.Project; - var newProject = project with - { - PackageReferences = newPackageReferences - }; + var newPackageReferences = project.PackageReferences + .ToDictionary(current => current.Key, current => current.Value); - await SaveProjectAsync(newProject); + newPackageReferences[packageReference.Id] = packageReference; - AppState.Project = newProject; - } - finally + var newProject = project with { - _projectSemaphore.Release(); - } - } + PackageReferences = newPackageReferences + }; - public async Task DeletePackageReferenceAsync( - Guid packageReferenceId) - { - await _projectSemaphore.WaitAsync(); + await SaveProjectAsync(newProject); - try - { - var project = AppState.Project; + AppState.Project = newProject; + } + finally + { + _projectSemaphore.Release(); + } + } - var newPackageReferences = project.PackageReferences - .ToDictionary(current => current.Key, current => current.Value); + public async Task DeletePackageReferenceAsync( + Guid packageReferenceId) + { + await _projectSemaphore.WaitAsync(); - newPackageReferences.Remove(packageReferenceId); + try + { + var project = AppState.Project; - var newProject = project with - { - PackageReferences = newPackageReferences - }; + var newPackageReferences = project.PackageReferences + .ToDictionary(current => current.Key, current => current.Value); - await SaveProjectAsync(newProject); + newPackageReferences.Remove(packageReferenceId); - AppState.Project = newProject; - } - finally + var newProject = project with { - _projectSemaphore.Release(); - } - } + PackageReferences = newPackageReferences + }; - public async Task PutDataSourceRegistrationAsync(string userId, InternalDataSourceRegistration registration) - { - await _projectSemaphore.WaitAsync(); + await SaveProjectAsync(newProject); - try - { - var project = AppState.Project; + AppState.Project = newProject; + } + finally + { + _projectSemaphore.Release(); + } + } - if (!project.UserConfigurations.TryGetValue(userId, out var userConfiguration)) - userConfiguration = new UserConfiguration(new Dictionary()); + public async Task PutDataSourceRegistrationAsync(string userId, InternalDataSourceRegistration registration) + { + await _projectSemaphore.WaitAsync(); - var newDataSourceRegistrations = userConfiguration.DataSourceRegistrations - .ToDictionary(current => current.Key, current => current.Value); + try + { + var project = AppState.Project; - newDataSourceRegistrations[registration.Id] = registration; + if (!project.UserConfigurations.TryGetValue(userId, out var userConfiguration)) + userConfiguration = new UserConfiguration(new Dictionary()); - var newUserConfiguration = userConfiguration with - { - DataSourceRegistrations = newDataSourceRegistrations - }; + var newDataSourceRegistrations = userConfiguration.DataSourceRegistrations + .ToDictionary(current => current.Key, current => current.Value); - var userConfigurations = project.UserConfigurations - .ToDictionary(current => current.Key, current => current.Value); + newDataSourceRegistrations[registration.Id] = registration; - userConfigurations[userId] = newUserConfiguration; + var newUserConfiguration = userConfiguration with + { + DataSourceRegistrations = newDataSourceRegistrations + }; - var newProject = project with - { - UserConfigurations = userConfigurations - }; + var userConfigurations = project.UserConfigurations + .ToDictionary(current => current.Key, current => current.Value); - await SaveProjectAsync(newProject); + userConfigurations[userId] = newUserConfiguration; - AppState.Project = newProject; - } - finally + var newProject = project with { - _projectSemaphore.Release(); - } - } + UserConfigurations = userConfigurations + }; - public async Task DeleteDataSourceRegistrationAsync(string username, Guid registrationId) - { - await _projectSemaphore.WaitAsync(); + await SaveProjectAsync(newProject); - try - { - var project = AppState.Project; + AppState.Project = newProject; + } + finally + { + _projectSemaphore.Release(); + } + } - if (!project.UserConfigurations.TryGetValue(username, out var userConfiguration)) - return; + public async Task DeleteDataSourceRegistrationAsync(string username, Guid registrationId) + { + await _projectSemaphore.WaitAsync(); - var newDataSourceRegistrations = userConfiguration.DataSourceRegistrations - .ToDictionary(current => current.Key, current => current.Value); + try + { + var project = AppState.Project; - newDataSourceRegistrations.Remove(registrationId); + if (!project.UserConfigurations.TryGetValue(username, out var userConfiguration)) + return; - var newUserConfiguration = userConfiguration with - { - DataSourceRegistrations = newDataSourceRegistrations - }; + var newDataSourceRegistrations = userConfiguration.DataSourceRegistrations + .ToDictionary(current => current.Key, current => current.Value); - var userConfigurations = project.UserConfigurations - .ToDictionary(current => current.Key, current => current.Value); + newDataSourceRegistrations.Remove(registrationId); - userConfigurations[username] = newUserConfiguration; + var newUserConfiguration = userConfiguration with + { + DataSourceRegistrations = newDataSourceRegistrations + }; - var newProject = project with - { - UserConfigurations = userConfigurations - }; + var userConfigurations = project.UserConfigurations + .ToDictionary(current => current.Key, current => current.Value); - await SaveProjectAsync(newProject); + userConfigurations[username] = newUserConfiguration; - AppState.Project = newProject; - } - finally + var newProject = project with { - _projectSemaphore.Release(); - } + UserConfigurations = userConfigurations + }; + + await SaveProjectAsync(newProject); + + AppState.Project = newProject; + } + finally + { + _projectSemaphore.Release(); } + } + + public async Task PutSystemConfigurationAsync(IReadOnlyDictionary? configuration) + { + await _projectSemaphore.WaitAsync(); - public async Task PutSystemConfigurationAsync(IReadOnlyDictionary? configuration) + try { - await _projectSemaphore.WaitAsync(); + var project = AppState.Project; - try + var newProject = project with { - var project = AppState.Project; + SystemConfiguration = configuration + }; - var newProject = project with - { - SystemConfiguration = configuration - }; + await SaveProjectAsync(newProject); - await SaveProjectAsync(newProject); - - AppState.Project = newProject; - } - finally - { - _projectSemaphore.Release(); - } + AppState.Project = newProject; } + finally + { + _projectSemaphore.Release(); + } + } - private void LoadDataWriters() + private void LoadDataWriters() + { + var labelsAndDescriptions = new List<(string Label, ExtensionDescription Description)>(); + + /* for each data writer */ + foreach (var dataWriterType in _extensionHive.GetExtensions()) { - var labelsAndDescriptions = new List<(string Label, ExtensionDescription Description)>(); + var fullName = dataWriterType.FullName!; + var attribute = dataWriterType.GetCustomAttribute(); - /* for each data writer */ - foreach (var dataWriterType in _extensionHive.GetExtensions()) + if (attribute is null) { - var fullName = dataWriterType.FullName!; - var attribute = dataWriterType.GetCustomAttribute(); - - if (attribute is null) - { - _logger.LogWarning("Data writer {DataWriter} has no description attribute", fullName); - continue; - } - - var additionalInformation = attribute.Description; - var label = additionalInformation?.GetStringValue(Nexus.UI.Core.Constants.DATA_WRITER_LABEL_KEY); - - if (label is null) - throw new Exception($"The description of data writer {fullName} has no label property"); - - var version = dataWriterType.Assembly - .GetCustomAttribute()! - .InformationalVersion; - - var attribute2 = dataWriterType - .GetCustomAttribute(inherit: false); - - if (attribute2 is null) - labelsAndDescriptions.Add((label, new ExtensionDescription( - fullName, - version, - default, - default, - default, - additionalInformation))); - - else - labelsAndDescriptions.Add((label, new ExtensionDescription( - fullName, - version, - attribute2.Description, - attribute2.ProjectUrl, - attribute2.RepositoryUrl, - additionalInformation))); + _logger.LogWarning("Data writer {DataWriter} has no description attribute", fullName); + continue; } - var dataWriterDescriptions = labelsAndDescriptions - .OrderBy(current => current.Label) - .Select(current => current.Description) - .ToList(); - - AppState.DataWriterDescriptions = dataWriterDescriptions; + var additionalInformation = attribute.Description; + var label = additionalInformation?.GetStringValue(Nexus.UI.Core.Constants.DATA_WRITER_LABEL_KEY); + + if (label is null) + throw new Exception($"The description of data writer {fullName} has no label property"); + + var version = dataWriterType.Assembly + .GetCustomAttribute()! + .InformationalVersion; + + var attribute2 = dataWriterType + .GetCustomAttribute(inherit: false); + + if (attribute2 is null) + labelsAndDescriptions.Add((label, new ExtensionDescription( + fullName, + version, + default, + default, + default, + additionalInformation))); + + else + labelsAndDescriptions.Add((label, new ExtensionDescription( + fullName, + version, + attribute2.Description, + attribute2.ProjectUrl, + attribute2.RepositoryUrl, + additionalInformation))); } - private async Task SaveProjectAsync(NexusProject project) - { - using var stream = _databaseService.WriteProject(); - await JsonSerializerHelper.SerializeIndentedAsync(stream, project); - } + var dataWriterDescriptions = labelsAndDescriptions + .OrderBy(current => current.Label) + .Select(current => current.Description) + .ToList(); - #endregion + AppState.DataWriterDescriptions = dataWriterDescriptions; } + + private async Task SaveProjectAsync(NexusProject project) + { + using var stream = _databaseService.WriteProject(); + await JsonSerializerHelper.SerializeIndentedAsync(stream, project); + } + + #endregion } diff --git a/src/Nexus/Services/CacheService.cs b/src/Nexus/Services/CacheService.cs index b19ceb24..12464a50 100644 --- a/src/Nexus/Services/CacheService.cs +++ b/src/Nexus/Services/CacheService.cs @@ -3,205 +3,204 @@ using Nexus.Utilities; using System.Globalization; -namespace Nexus.Services +namespace Nexus.Services; + +internal interface ICacheService { - internal interface ICacheService + Task> ReadAsync( + CatalogItem catalogItem, + DateTime begin, + Memory targetBuffer, + CancellationToken cancellationToken); + + Task UpdateAsync( + CatalogItem catalogItem, + DateTime begin, + Memory sourceBuffer, + List uncachedIntervals, + CancellationToken cancellationToken); + + Task ClearAsync( + string catalogId, + DateTime begin, + DateTime end, + IProgress progress, + CancellationToken cancellationToken); +} + +internal class CacheService : ICacheService +{ + private readonly IDatabaseService _databaseService; + private readonly TimeSpan _largestSamplePeriod = TimeSpan.FromDays(1); + + public CacheService( + IDatabaseService databaseService) { - Task> ReadAsync( - CatalogItem catalogItem, - DateTime begin, - Memory targetBuffer, - CancellationToken cancellationToken); - - Task UpdateAsync( - CatalogItem catalogItem, - DateTime begin, - Memory sourceBuffer, - List uncachedIntervals, - CancellationToken cancellationToken); - - Task ClearAsync( - string catalogId, - DateTime begin, - DateTime end, - IProgress progress, - CancellationToken cancellationToken); + _databaseService = databaseService; } - internal class CacheService : ICacheService + public async Task> ReadAsync( + CatalogItem catalogItem, + DateTime begin, + Memory targetBuffer, + CancellationToken cancellationToken) { - private readonly IDatabaseService _databaseService; - private readonly TimeSpan _largestSamplePeriod = TimeSpan.FromDays(1); + var samplePeriod = catalogItem.Representation.SamplePeriod; + var end = begin + samplePeriod * targetBuffer.Length; + var filePeriod = GetFilePeriod(samplePeriod); + var uncachedIntervals = new List(); - public CacheService( - IDatabaseService databaseService) + /* try read data from cache */ + await NexusUtilities.FileLoopAsync(begin, end, filePeriod, async (fileBegin, fileOffset, duration) => { - _databaseService = databaseService; - } - - public async Task> ReadAsync( - CatalogItem catalogItem, - DateTime begin, - Memory targetBuffer, - CancellationToken cancellationToken) - { - var samplePeriod = catalogItem.Representation.SamplePeriod; - var end = begin + samplePeriod * targetBuffer.Length; - var filePeriod = GetFilePeriod(samplePeriod); - var uncachedIntervals = new List(); + var actualBegin = fileBegin + fileOffset; + var actualEnd = actualBegin + duration; - /* try read data from cache */ - await NexusUtilities.FileLoopAsync(begin, end, filePeriod, async (fileBegin, fileOffset, duration) => + if (_databaseService.TryReadCacheEntry(catalogItem, fileBegin, out var cacheEntry)) { - var actualBegin = fileBegin + fileOffset; - var actualEnd = actualBegin + duration; + var slicedTargetBuffer = targetBuffer.Slice( + start: NexusUtilities.Scale(actualBegin - begin, samplePeriod), + length: NexusUtilities.Scale(duration, samplePeriod)); - if (_databaseService.TryReadCacheEntry(catalogItem, fileBegin, out var cacheEntry)) + try { - var slicedTargetBuffer = targetBuffer.Slice( - start: NexusUtilities.Scale(actualBegin - begin, samplePeriod), - length: NexusUtilities.Scale(duration, samplePeriod)); + using var cacheEntryWrapper = new CacheEntryWrapper( + fileBegin, filePeriod, samplePeriod, cacheEntry); - try - { - using var cacheEntryWrapper = new CacheEntryWrapper( - fileBegin, filePeriod, samplePeriod, cacheEntry); + var moreUncachedIntervals = await cacheEntryWrapper.ReadAsync( + actualBegin, + actualEnd, + slicedTargetBuffer, + cancellationToken); - var moreUncachedIntervals = await cacheEntryWrapper.ReadAsync( - actualBegin, - actualEnd, - slicedTargetBuffer, - cancellationToken); - - uncachedIntervals.AddRange(moreUncachedIntervals); - } - catch - { - uncachedIntervals.Add(new Interval(actualBegin, actualEnd)); - } + uncachedIntervals.AddRange(moreUncachedIntervals); } - - else + catch { uncachedIntervals.Add(new Interval(actualBegin, actualEnd)); } - }); - - var consolidatedIntervals = new List(); + } - /* consolidate intervals */ - if (uncachedIntervals.Count >= 1) + else { - consolidatedIntervals.Add(uncachedIntervals[0]); + uncachedIntervals.Add(new Interval(actualBegin, actualEnd)); + } + }); - for (int i = 1; i < uncachedIntervals.Count; i++) - { - if (consolidatedIntervals[^1].End == uncachedIntervals[i].Begin) - consolidatedIntervals[^1] = consolidatedIntervals[^1] with { End = uncachedIntervals[i].End }; + var consolidatedIntervals = new List(); - else - consolidatedIntervals.Add(uncachedIntervals[i]); - } - } + /* consolidate intervals */ + if (uncachedIntervals.Count >= 1) + { + consolidatedIntervals.Add(uncachedIntervals[0]); + + for (int i = 1; i < uncachedIntervals.Count; i++) + { + if (consolidatedIntervals[^1].End == uncachedIntervals[i].Begin) + consolidatedIntervals[^1] = consolidatedIntervals[^1] with { End = uncachedIntervals[i].End }; - return consolidatedIntervals; + else + consolidatedIntervals.Add(uncachedIntervals[i]); + } } - public async Task UpdateAsync( - CatalogItem catalogItem, - DateTime begin, - Memory sourceBuffer, - List uncachedIntervals, - CancellationToken cancellationToken) - { - var samplePeriod = catalogItem.Representation.SamplePeriod; - var filePeriod = GetFilePeriod(samplePeriod); + return consolidatedIntervals; + } + + public async Task UpdateAsync( + CatalogItem catalogItem, + DateTime begin, + Memory sourceBuffer, + List uncachedIntervals, + CancellationToken cancellationToken) + { + var samplePeriod = catalogItem.Representation.SamplePeriod; + var filePeriod = GetFilePeriod(samplePeriod); - /* try write data to cache */ - foreach (var interval in uncachedIntervals) + /* try write data to cache */ + foreach (var interval in uncachedIntervals) + { + await NexusUtilities.FileLoopAsync(interval.Begin, interval.End, filePeriod, async (fileBegin, fileOffset, duration) => { - await NexusUtilities.FileLoopAsync(interval.Begin, interval.End, filePeriod, async (fileBegin, fileOffset, duration) => + var actualBegin = fileBegin + fileOffset; + + if (_databaseService.TryWriteCacheEntry(catalogItem, fileBegin, out var cacheEntry)) { - var actualBegin = fileBegin + fileOffset; + var slicedSourceBuffer = sourceBuffer.Slice( + start: NexusUtilities.Scale(actualBegin - begin, samplePeriod), + length: NexusUtilities.Scale(duration, samplePeriod)); - if (_databaseService.TryWriteCacheEntry(catalogItem, fileBegin, out var cacheEntry)) + try { - var slicedSourceBuffer = sourceBuffer.Slice( - start: NexusUtilities.Scale(actualBegin - begin, samplePeriod), - length: NexusUtilities.Scale(duration, samplePeriod)); - - try - { - using var cacheEntryWrapper = new CacheEntryWrapper( - fileBegin, filePeriod, samplePeriod, cacheEntry); - - await cacheEntryWrapper.WriteAsync( - actualBegin, - slicedSourceBuffer, - cancellationToken); - } - catch - { - // - } + using var cacheEntryWrapper = new CacheEntryWrapper( + fileBegin, filePeriod, samplePeriod, cacheEntry); + + await cacheEntryWrapper.WriteAsync( + actualBegin, + slicedSourceBuffer, + cancellationToken); } - }); - } + catch + { + // + } + } + }); } + } + + public async Task ClearAsync( + string catalogId, + DateTime begin, + DateTime end, + IProgress progress, + CancellationToken cancellationToken) + { + var currentProgress = 0.0; + var totalPeriod = end - begin; + var folderPeriod = TimeSpan.FromDays(1); + var timeout = TimeSpan.FromMinutes(1); - public async Task ClearAsync( - string catalogId, - DateTime begin, - DateTime end, - IProgress progress, - CancellationToken cancellationToken) + await NexusUtilities.FileLoopAsync(begin, end, folderPeriod, async (folderBegin, folderOffset, duration) => { - var currentProgress = 0.0; - var totalPeriod = end - begin; - var folderPeriod = TimeSpan.FromDays(1); - var timeout = TimeSpan.FromMinutes(1); + cancellationToken.ThrowIfCancellationRequested(); - await NexusUtilities.FileLoopAsync(begin, end, folderPeriod, async (folderBegin, folderOffset, duration) => - { - cancellationToken.ThrowIfCancellationRequested(); + var dateOnly = DateOnly.FromDateTime(folderBegin.Date); - var dateOnly = DateOnly.FromDateTime(folderBegin.Date); + /* partial day */ + if (duration != folderPeriod) + await _databaseService.ClearCacheEntriesAsync(catalogId, dateOnly, timeout, cacheEntryId => + { + var dateTimeString = Path + .GetFileName(cacheEntryId)[..27]; - /* partial day */ - if (duration != folderPeriod) - await _databaseService.ClearCacheEntriesAsync(catalogId, dateOnly, timeout, cacheEntryId => - { - var dateTimeString = Path - .GetFileName(cacheEntryId)[..27]; + var cacheEntryDateTime = DateTime + .ParseExact(dateTimeString, "yyyy-MM-ddTHH-mm-ss-fffffff", CultureInfo.InvariantCulture); - var cacheEntryDateTime = DateTime - .ParseExact(dateTimeString, "yyyy-MM-ddTHH-mm-ss-fffffff", CultureInfo.InvariantCulture); + return begin <= cacheEntryDateTime && cacheEntryDateTime < end; + }); - return begin <= cacheEntryDateTime && cacheEntryDateTime < end; - }); + /* full day */ + else + await _databaseService.ClearCacheEntriesAsync(catalogId, dateOnly, timeout, cacheEntryId => true); - /* full day */ - else - await _databaseService.ClearCacheEntriesAsync(catalogId, dateOnly, timeout, cacheEntryId => true); + var currentEnd = folderBegin + folderOffset + duration; + currentProgress = (currentEnd - begin).Ticks / (double)totalPeriod.Ticks; + progress.Report(currentProgress); + }); + } - var currentEnd = folderBegin + folderOffset + duration; - currentProgress = (currentEnd - begin).Ticks / (double)totalPeriod.Ticks; - progress.Report(currentProgress); - }); - } + private TimeSpan GetFilePeriod(TimeSpan samplePeriod) + { + if (samplePeriod > _largestSamplePeriod || TimeSpan.FromDays(1).Ticks % samplePeriod.Ticks != 0) + throw new Exception("Caching is only supported for sample periods fit exactly into a single day."); - private TimeSpan GetFilePeriod(TimeSpan samplePeriod) + return samplePeriod switch { - if (samplePeriod > _largestSamplePeriod || TimeSpan.FromDays(1).Ticks % samplePeriod.Ticks != 0) - throw new Exception("Caching is only supported for sample periods fit exactly into a single day."); - - return samplePeriod switch - { - _ when samplePeriod <= TimeSpan.FromSeconds(1e-9) => TimeSpan.FromSeconds(1e-3), - _ when samplePeriod <= TimeSpan.FromSeconds(1e-6) => TimeSpan.FromSeconds(1e+0), - _ when samplePeriod <= TimeSpan.FromSeconds(1e-3) => TimeSpan.FromHours(1), - _ => TimeSpan.FromDays(1), - }; - } + _ when samplePeriod <= TimeSpan.FromSeconds(1e-9) => TimeSpan.FromSeconds(1e-3), + _ when samplePeriod <= TimeSpan.FromSeconds(1e-6) => TimeSpan.FromSeconds(1e+0), + _ when samplePeriod <= TimeSpan.FromSeconds(1e-3) => TimeSpan.FromHours(1), + _ => TimeSpan.FromDays(1), + }; } } diff --git a/src/Nexus/Services/CatalogManager.cs b/src/Nexus/Services/CatalogManager.cs index 34441fe4..2da4aa5f 100644 --- a/src/Nexus/Services/CatalogManager.cs +++ b/src/Nexus/Services/CatalogManager.cs @@ -7,317 +7,316 @@ using System.Text.Json; using static OpenIddict.Abstractions.OpenIddictConstants; -namespace Nexus.Services +namespace Nexus.Services; + +internal interface ICatalogManager { - internal interface ICatalogManager - { - Task GetCatalogContainersAsync( - CatalogContainer parent, - CancellationToken cancellationToken); - } + Task GetCatalogContainersAsync( + CatalogContainer parent, + CancellationToken cancellationToken); +} - internal class CatalogManager : ICatalogManager - { - #region Types +internal class CatalogManager : ICatalogManager +{ + #region Types - record CatalogPrototype( - CatalogRegistration Registration, - InternalDataSourceRegistration DataSourceRegistration, - InternalPackageReference PackageReference, - CatalogMetadata Metadata, - ClaimsPrincipal? Owner); + record CatalogPrototype( + CatalogRegistration Registration, + InternalDataSourceRegistration DataSourceRegistration, + InternalPackageReference PackageReference, + CatalogMetadata Metadata, + ClaimsPrincipal? Owner); - #endregion + #endregion - #region Fields + #region Fields - private readonly AppState _appState; - private readonly IDataControllerService _dataControllerService; - private readonly IDatabaseService _databaseService; - private readonly IServiceProvider _serviceProvider; - private readonly IExtensionHive _extensionHive; - private readonly ILogger _logger; + private readonly AppState _appState; + private readonly IDataControllerService _dataControllerService; + private readonly IDatabaseService _databaseService; + private readonly IServiceProvider _serviceProvider; + private readonly IExtensionHive _extensionHive; + private readonly ILogger _logger; - #endregion + #endregion - #region Constructors + #region Constructors - public CatalogManager( - AppState appState, - IDataControllerService dataControllerService, - IDatabaseService databaseService, - IServiceProvider serviceProvider, - IExtensionHive extensionHive, - ILogger logger) - { - _appState = appState; - _dataControllerService = dataControllerService; - _databaseService = databaseService; - _serviceProvider = serviceProvider; - _extensionHive = extensionHive; - _logger = logger; - } + public CatalogManager( + AppState appState, + IDataControllerService dataControllerService, + IDatabaseService databaseService, + IServiceProvider serviceProvider, + IExtensionHive extensionHive, + ILogger logger) + { + _appState = appState; + _dataControllerService = dataControllerService; + _databaseService = databaseService; + _serviceProvider = serviceProvider; + _extensionHive = extensionHive; + _logger = logger; + } - #endregion + #endregion - #region Methods + #region Methods - public async Task GetCatalogContainersAsync( - CatalogContainer parent, - CancellationToken cancellationToken) + public async Task GetCatalogContainersAsync( + CatalogContainer parent, + CancellationToken cancellationToken) + { + CatalogContainer[] catalogContainers; + + using var loggerScope = _logger.BeginScope(new Dictionary() { - CatalogContainer[] catalogContainers; + ["ParentCatalogId"] = parent.Id + }); - using var loggerScope = _logger.BeginScope(new Dictionary() + /* special case: root */ + if (parent.Id == CatalogContainer.RootCatalogId) + { + /* load builtin data source */ + var builtinDataSourceRegistrations = new InternalDataSourceRegistration[] { - ["ParentCatalogId"] = parent.Id - }); - - /* special case: root */ - if (parent.Id == CatalogContainer.RootCatalogId) + new InternalDataSourceRegistration( + Id: Sample.RegistrationId, + Type: typeof(Sample).FullName!, + ResourceLocator: default, + Configuration: default) + }; + + /* load all catalog identifiers */ + var path = CatalogContainer.RootCatalogId; + var catalogPrototypes = new List(); + + /* => for the built-in data source registrations */ + + // TODO: Load Parallel? + /* for each data source registration */ + foreach (var registration in builtinDataSourceRegistrations) { - /* load builtin data source */ - var builtinDataSourceRegistrations = new InternalDataSourceRegistration[] - { - new InternalDataSourceRegistration( - Id: Sample.RegistrationId, - Type: typeof(Sample).FullName!, - ResourceLocator: default, - Configuration: default) - }; + using var controller = await _dataControllerService.GetDataSourceControllerAsync(registration, cancellationToken); + var catalogRegistrations = await controller.GetCatalogRegistrationsAsync(path, cancellationToken); + var packageReference = _extensionHive.GetPackageReference(registration.Type); - /* load all catalog identifiers */ - var path = CatalogContainer.RootCatalogId; - var catalogPrototypes = new List(); - - /* => for the built-in data source registrations */ - - // TODO: Load Parallel? - /* for each data source registration */ - foreach (var registration in builtinDataSourceRegistrations) + foreach (var catalogRegistration in catalogRegistrations) { - using var controller = await _dataControllerService.GetDataSourceControllerAsync(registration, cancellationToken); - var catalogRegistrations = await controller.GetCatalogRegistrationsAsync(path, cancellationToken); - var packageReference = _extensionHive.GetPackageReference(registration.Type); - - foreach (var catalogRegistration in catalogRegistrations) - { - var metadata = LoadMetadata(catalogRegistration.Path); + var metadata = LoadMetadata(catalogRegistration.Path); - var catalogPrototype = new CatalogPrototype( - catalogRegistration, - registration, - packageReference, - metadata, - null); + var catalogPrototype = new CatalogPrototype( + catalogRegistration, + registration, + packageReference, + metadata, + null); - catalogPrototypes.Add(catalogPrototype); - } + catalogPrototypes.Add(catalogPrototype); } + } - using var scope = _serviceProvider.CreateScope(); - var dbService = scope.ServiceProvider.GetRequiredService(); + using var scope = _serviceProvider.CreateScope(); + var dbService = scope.ServiceProvider.GetRequiredService(); - /* => for each user with existing config */ - foreach (var (userId, userConfiguration) in _appState.Project.UserConfigurations) - { - // get owner - var user = await dbService.FindUserAsync(userId); + /* => for each user with existing config */ + foreach (var (userId, userConfiguration) in _appState.Project.UserConfigurations) + { + // get owner + var user = await dbService.FindUserAsync(userId); - if (user is null) - continue; + if (user is null) + continue; - var claims = user.Claims - .Select(claim => new Claim(claim.Type, claim.Value)) - .ToList(); + var claims = user.Claims + .Select(claim => new Claim(claim.Type, claim.Value)) + .ToList(); - claims - .Add(new Claim(Claims.Subject, userId)); + claims + .Add(new Claim(Claims.Subject, userId)); - var owner = new ClaimsPrincipal( - new ClaimsIdentity( - claims, - authenticationType: "Fake authentication type", - nameType: Claims.Name, - roleType: Claims.Role)); + var owner = new ClaimsPrincipal( + new ClaimsIdentity( + claims, + authenticationType: "Fake authentication type", + nameType: Claims.Name, + roleType: Claims.Role)); - /* for each data source registration */ - foreach (var registration in userConfiguration.DataSourceRegistrations.Values) + /* for each data source registration */ + foreach (var registration in userConfiguration.DataSourceRegistrations.Values) + { + try { - try - { - using var controller = await _dataControllerService.GetDataSourceControllerAsync(registration, cancellationToken); - var catalogRegistrations = await controller.GetCatalogRegistrationsAsync(path, cancellationToken); - var packageReference = _extensionHive.GetPackageReference(registration.Type); - - foreach (var catalogRegistration in catalogRegistrations) - { - var metadata = LoadMetadata(catalogRegistration.Path); - - var prototype = new CatalogPrototype( - catalogRegistration, - registration, - packageReference, - metadata, - owner); - - catalogPrototypes.Add(prototype); - } - } - catch (Exception ex) + using var controller = await _dataControllerService.GetDataSourceControllerAsync(registration, cancellationToken); + var catalogRegistrations = await controller.GetCatalogRegistrationsAsync(path, cancellationToken); + var packageReference = _extensionHive.GetPackageReference(registration.Type); + + foreach (var catalogRegistration in catalogRegistrations) { - _logger.LogWarning(ex, "Unable to get or process data source registration for user {Username}", user.Name); + var metadata = LoadMetadata(catalogRegistration.Path); + + var prototype = new CatalogPrototype( + catalogRegistration, + registration, + packageReference, + metadata, + owner); + + catalogPrototypes.Add(prototype); } } + catch (Exception ex) + { + _logger.LogWarning(ex, "Unable to get or process data source registration for user {Username}", user.Name); + } } - - catalogContainers = ProcessCatalogPrototypes(catalogPrototypes.ToArray()); - _logger.LogInformation("Found {CatalogCount} top level catalogs", catalogContainers.Length); } - /* all other catalogs */ - else - { - using var controller = await _dataControllerService - .GetDataSourceControllerAsync(parent.DataSourceRegistration, cancellationToken); + catalogContainers = ProcessCatalogPrototypes(catalogPrototypes.ToArray()); + _logger.LogInformation("Found {CatalogCount} top level catalogs", catalogContainers.Length); + } - /* Why trailing slash? - * Because we want the "directory content" (see the "ls /home/karl/" example here: - * https://stackoverflow.com/questions/980255/should-a-directory-path-variable-end-with-a-trailing-slash) - */ + /* all other catalogs */ + else + { + using var controller = await _dataControllerService + .GetDataSourceControllerAsync(parent.DataSourceRegistration, cancellationToken); - try - { - var catalogRegistrations = await controller - .GetCatalogRegistrationsAsync(parent.Id + "/", cancellationToken); + /* Why trailing slash? + * Because we want the "directory content" (see the "ls /home/karl/" example here: + * https://stackoverflow.com/questions/980255/should-a-directory-path-variable-end-with-a-trailing-slash) + */ - var prototypes = catalogRegistrations - .Select(catalogRegistration => - { - var metadata = LoadMetadata(catalogRegistration.Path); - - return new CatalogPrototype( - catalogRegistration, - parent.DataSourceRegistration, - parent.PackageReference, - metadata, - parent.Owner); - }); + try + { + var catalogRegistrations = await controller + .GetCatalogRegistrationsAsync(parent.Id + "/", cancellationToken); - catalogContainers = ProcessCatalogPrototypes(prototypes.ToArray()); - } - catch (Exception ex) + var prototypes = catalogRegistrations + .Select(catalogRegistration => { - _logger.LogWarning(ex, "Unable to get or process child data source registrations"); - catalogContainers = Array.Empty(); - } - } + var metadata = LoadMetadata(catalogRegistration.Path); - return catalogContainers; - } + return new CatalogPrototype( + catalogRegistration, + parent.DataSourceRegistration, + parent.PackageReference, + metadata, + parent.Owner); + }); - private CatalogContainer[] ProcessCatalogPrototypes( - IEnumerable catalogPrototypes) - { - /* clean up */ - catalogPrototypes = EnsureNoHierarchy(catalogPrototypes); - - /* convert to catalog containers */ - var catalogContainers = catalogPrototypes.Select(prototype => + catalogContainers = ProcessCatalogPrototypes(prototypes.ToArray()); + } + catch (Exception ex) { - /* create catalog container */ - var catalogContainer = new CatalogContainer( - prototype.Registration, - prototype.Owner, - prototype.DataSourceRegistration, - prototype.PackageReference, - prototype.Metadata, - this, - _databaseService, - _dataControllerService); - - return catalogContainer; - }); - - return catalogContainers.ToArray(); + _logger.LogWarning(ex, "Unable to get or process child data source registrations"); + catalogContainers = Array.Empty(); + } } - private CatalogMetadata LoadMetadata(string catalogId) + return catalogContainers; + } + + private CatalogContainer[] ProcessCatalogPrototypes( + IEnumerable catalogPrototypes) + { + /* clean up */ + catalogPrototypes = EnsureNoHierarchy(catalogPrototypes); + + /* convert to catalog containers */ + var catalogContainers = catalogPrototypes.Select(prototype => { - if (_databaseService.TryReadCatalogMetadata(catalogId, out var jsonString)) - return JsonSerializer.Deserialize(jsonString) ?? throw new Exception("catalogMetadata is null"); + /* create catalog container */ + var catalogContainer = new CatalogContainer( + prototype.Registration, + prototype.Owner, + prototype.DataSourceRegistration, + prototype.PackageReference, + prototype.Metadata, + this, + _databaseService, + _dataControllerService); + + return catalogContainer; + }); + + return catalogContainers.ToArray(); + } - else - return new CatalogMetadata(default, default, default); - } + private CatalogMetadata LoadMetadata(string catalogId) + { + if (_databaseService.TryReadCatalogMetadata(catalogId, out var jsonString)) + return JsonSerializer.Deserialize(jsonString) ?? throw new Exception("catalogMetadata is null"); - private CatalogPrototype[] EnsureNoHierarchy( - IEnumerable catalogPrototypes) + else + return new CatalogMetadata(default, default, default); + } + + private CatalogPrototype[] EnsureNoHierarchy( + IEnumerable catalogPrototypes) + { + // Background: + // + // Nexus allows catalogs to have child catalogs like folders in a file system. To simplify things, + // it is required that a catalog that comes from a certain data source can only have child + // catalogs of the very same data source. + // + // In general, child catalogs will be loaded lazily. Therefore, for any catalog of the provided array that + // appears to be a child catalog, it can be assumed it comes from a data source other than the one + // from the parent catalog. Depending on the user's rights, this method decides which one will survive. + // + // + // Example: + // + // The following combination of catalogs is allowed: + // data source 1: /a + /a/a + /a/b + // data source 2: /a2/c + // + // The following combination of catalogs is forbidden: + // data source 1: /a + /a/a + /a/b + // data source 2: /a/c + + var catalogPrototypesToKeep = new List(); + + foreach (var catalogPrototype in catalogPrototypes) { - // Background: - // - // Nexus allows catalogs to have child catalogs like folders in a file system. To simplify things, - // it is required that a catalog that comes from a certain data source can only have child - // catalogs of the very same data source. - // - // In general, child catalogs will be loaded lazily. Therefore, for any catalog of the provided array that - // appears to be a child catalog, it can be assumed it comes from a data source other than the one - // from the parent catalog. Depending on the user's rights, this method decides which one will survive. - // - // - // Example: - // - // The following combination of catalogs is allowed: - // data source 1: /a + /a/a + /a/b - // data source 2: /a2/c - // - // The following combination of catalogs is forbidden: - // data source 1: /a + /a/a + /a/b - // data source 2: /a/c - - var catalogPrototypesToKeep = new List(); - - foreach (var catalogPrototype in catalogPrototypes) - { - var referenceIndex = catalogPrototypesToKeep.FindIndex( - current => - { - var currentCatalogId = current.Registration.Path + '/'; - var prototypeCatalogId = catalogPrototype.Registration.Path + '/'; + var referenceIndex = catalogPrototypesToKeep.FindIndex( + current => + { + var currentCatalogId = current.Registration.Path + '/'; + var prototypeCatalogId = catalogPrototype.Registration.Path + '/'; - return currentCatalogId.StartsWith(prototypeCatalogId, StringComparison.OrdinalIgnoreCase) || - prototypeCatalogId.StartsWith(currentCatalogId, StringComparison.OrdinalIgnoreCase); - }); + return currentCatalogId.StartsWith(prototypeCatalogId, StringComparison.OrdinalIgnoreCase) || + prototypeCatalogId.StartsWith(currentCatalogId, StringComparison.OrdinalIgnoreCase); + }); - /* nothing found */ - if (referenceIndex < 0) - { - catalogPrototypesToKeep.Add(catalogPrototype); - } + /* nothing found */ + if (referenceIndex < 0) + { + catalogPrototypesToKeep.Add(catalogPrototype); + } - /* reference found */ - else - { - var owner = catalogPrototype.Owner; - var ownerCanWrite = owner is null - || AuthUtilities.IsCatalogWritable(catalogPrototype.Registration.Path, catalogPrototype.Metadata, owner); + /* reference found */ + else + { + var owner = catalogPrototype.Owner; + var ownerCanWrite = owner is null + || AuthUtilities.IsCatalogWritable(catalogPrototype.Registration.Path, catalogPrototype.Metadata, owner); - var otherPrototype = catalogPrototypesToKeep[referenceIndex]; - var otherOwner = otherPrototype.Owner; - var otherOwnerCanWrite = otherOwner is null - || AuthUtilities.IsCatalogWritable(otherPrototype.Registration.Path, catalogPrototype.Metadata, otherOwner); + var otherPrototype = catalogPrototypesToKeep[referenceIndex]; + var otherOwner = otherPrototype.Owner; + var otherOwnerCanWrite = otherOwner is null + || AuthUtilities.IsCatalogWritable(otherPrototype.Registration.Path, catalogPrototype.Metadata, otherOwner); - if (!otherOwnerCanWrite && ownerCanWrite) - { - _logger.LogWarning("Duplicate catalog {CatalogId}", catalogPrototypesToKeep[referenceIndex]); - catalogPrototypesToKeep[referenceIndex] = catalogPrototype; - } + if (!otherOwnerCanWrite && ownerCanWrite) + { + _logger.LogWarning("Duplicate catalog {CatalogId}", catalogPrototypesToKeep[referenceIndex]); + catalogPrototypesToKeep[referenceIndex] = catalogPrototype; } } - - return catalogPrototypesToKeep.ToArray(); } - #endregion + return catalogPrototypesToKeep.ToArray(); } + + #endregion } diff --git a/src/Nexus/Services/DataControllerService.cs b/src/Nexus/Services/DataControllerService.cs index 24e2c874..36eeb7ec 100644 --- a/src/Nexus/Services/DataControllerService.cs +++ b/src/Nexus/Services/DataControllerService.cs @@ -5,128 +5,127 @@ using Nexus.DataModel; using Nexus.Extensibility; -namespace Nexus.Services +namespace Nexus.Services; + +internal interface IDataControllerService { - internal interface IDataControllerService + Task GetDataSourceControllerAsync( + InternalDataSourceRegistration registration, + CancellationToken cancellationToken); + + Task GetDataWriterControllerAsync( + Uri resourceLocator, + ExportParameters exportParameters, + CancellationToken cancellationToken); +} + +internal class DataControllerService : IDataControllerService +{ + public const string NexusConfigurationHeaderKey = "Nexus-Configuration"; + + private readonly AppState _appState; + private readonly DataOptions _dataOptions; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IExtensionHive _extensionHive; + private readonly IProcessingService _processingService; + private readonly ICacheService _cacheService; + private readonly ILogger _logger; + private readonly ILoggerFactory _loggerFactory; + + public DataControllerService( + AppState appState, + IHttpContextAccessor httpContextAccessor, + IExtensionHive extensionHive, + IProcessingService processingService, + ICacheService cacheService, + IOptions dataOptions, + ILogger logger, + ILoggerFactory loggerFactory) { - Task GetDataSourceControllerAsync( - InternalDataSourceRegistration registration, - CancellationToken cancellationToken); - - Task GetDataWriterControllerAsync( - Uri resourceLocator, - ExportParameters exportParameters, - CancellationToken cancellationToken); + _appState = appState; + _httpContextAccessor = httpContextAccessor; + _extensionHive = extensionHive; + _processingService = processingService; + _cacheService = cacheService; + _dataOptions = dataOptions.Value; + _logger = logger; + _loggerFactory = loggerFactory; } - internal class DataControllerService : IDataControllerService + public async Task GetDataSourceControllerAsync( + InternalDataSourceRegistration registration, + CancellationToken cancellationToken) { - public const string NexusConfigurationHeaderKey = "Nexus-Configuration"; - - private readonly AppState _appState; - private readonly DataOptions _dataOptions; - private readonly IHttpContextAccessor _httpContextAccessor; - private readonly IExtensionHive _extensionHive; - private readonly IProcessingService _processingService; - private readonly ICacheService _cacheService; - private readonly ILogger _logger; - private readonly ILoggerFactory _loggerFactory; - - public DataControllerService( - AppState appState, - IHttpContextAccessor httpContextAccessor, - IExtensionHive extensionHive, - IProcessingService processingService, - ICacheService cacheService, - IOptions dataOptions, - ILogger logger, - ILoggerFactory loggerFactory) - { - _appState = appState; - _httpContextAccessor = httpContextAccessor; - _extensionHive = extensionHive; - _processingService = processingService; - _cacheService = cacheService; - _dataOptions = dataOptions.Value; - _logger = logger; - _loggerFactory = loggerFactory; - } + var logger1 = _loggerFactory.CreateLogger(); + var logger2 = _loggerFactory.CreateLogger($"{registration.Type} - {registration.ResourceLocator?.ToString() ?? ""}"); - public async Task GetDataSourceControllerAsync( - InternalDataSourceRegistration registration, - CancellationToken cancellationToken) - { - var logger1 = _loggerFactory.CreateLogger(); - var logger2 = _loggerFactory.CreateLogger($"{registration.Type} - {registration.ResourceLocator?.ToString() ?? ""}"); + var dataSource = _extensionHive.GetInstance(registration.Type); + var requestConfiguration = GetRequestConfiguration(); - var dataSource = _extensionHive.GetInstance(registration.Type); - var requestConfiguration = GetRequestConfiguration(); + var clonedSystemConfiguration = _appState.Project.SystemConfiguration is null + ? default + : _appState.Project.SystemConfiguration.ToDictionary(entry => entry.Key, entry => entry.Value.Clone()); - var clonedSystemConfiguration = _appState.Project.SystemConfiguration is null - ? default - : _appState.Project.SystemConfiguration.ToDictionary(entry => entry.Key, entry => entry.Value.Clone()); + var controller = new DataSourceController( + dataSource, + registration, + systemConfiguration: clonedSystemConfiguration, + requestConfiguration: requestConfiguration, + _processingService, + _cacheService, + _dataOptions, + logger1); - var controller = new DataSourceController( - dataSource, - registration, - systemConfiguration: clonedSystemConfiguration, - requestConfiguration: requestConfiguration, - _processingService, - _cacheService, - _dataOptions, - logger1); + var actualCatalogCache = _appState.CatalogState.Cache.GetOrAdd( + registration, + registration => new ConcurrentDictionary()); - var actualCatalogCache = _appState.CatalogState.Cache.GetOrAdd( - registration, - registration => new ConcurrentDictionary()); + await controller.InitializeAsync(actualCatalogCache, logger2, cancellationToken); - await controller.InitializeAsync(actualCatalogCache, logger2, cancellationToken); + return controller; + } - return controller; - } + public async Task GetDataWriterControllerAsync(Uri resourceLocator, ExportParameters exportParameters, CancellationToken cancellationToken) + { + var logger1 = _loggerFactory.CreateLogger(); + var logger2 = _loggerFactory.CreateLogger($"{exportParameters.Type} - {resourceLocator}"); + var dataWriter = _extensionHive.GetInstance(exportParameters.Type ?? throw new Exception("The type must not be null.")); + var requestConfiguration = exportParameters.Configuration; - public async Task GetDataWriterControllerAsync(Uri resourceLocator, ExportParameters exportParameters, CancellationToken cancellationToken) - { - var logger1 = _loggerFactory.CreateLogger(); - var logger2 = _loggerFactory.CreateLogger($"{exportParameters.Type} - {resourceLocator}"); - var dataWriter = _extensionHive.GetInstance(exportParameters.Type ?? throw new Exception("The type must not be null.")); - var requestConfiguration = exportParameters.Configuration; + var clonedSystemConfiguration = _appState.Project.SystemConfiguration is null + ? default + : _appState.Project.SystemConfiguration.ToDictionary(entry => entry.Key, entry => entry.Value.Clone()); - var clonedSystemConfiguration = _appState.Project.SystemConfiguration is null - ? default - : _appState.Project.SystemConfiguration.ToDictionary(entry => entry.Key, entry => entry.Value.Clone()); + var controller = new DataWriterController( + dataWriter, + resourceLocator, + systemConfiguration: clonedSystemConfiguration, + requestConfiguration: requestConfiguration, + logger1); - var controller = new DataWriterController( - dataWriter, - resourceLocator, - systemConfiguration: clonedSystemConfiguration, - requestConfiguration: requestConfiguration, - logger1); + await controller.InitializeAsync(logger2, cancellationToken); - await controller.InitializeAsync(logger2, cancellationToken); + return controller; + } - return controller; - } + private IReadOnlyDictionary? GetRequestConfiguration() + { + var httpContext = _httpContextAccessor.HttpContext; - private IReadOnlyDictionary? GetRequestConfiguration() + if (httpContext is not null && + httpContext.Request.Headers.TryGetValue(NexusConfigurationHeaderKey, out var encodedRequestConfiguration)) { - var httpContext = _httpContextAccessor.HttpContext; - - if (httpContext is not null && - httpContext.Request.Headers.TryGetValue(NexusConfigurationHeaderKey, out var encodedRequestConfiguration)) - { - var firstEncodedRequestConfiguration = encodedRequestConfiguration.First(); + var firstEncodedRequestConfiguration = encodedRequestConfiguration.First(); - if (firstEncodedRequestConfiguration is null) - return default; + if (firstEncodedRequestConfiguration is null) + return default; - var requestConfiguration = JsonSerializer - .Deserialize>(Convert.FromBase64String(firstEncodedRequestConfiguration)); + var requestConfiguration = JsonSerializer + .Deserialize>(Convert.FromBase64String(firstEncodedRequestConfiguration)); - return requestConfiguration; - } - - return default; + return requestConfiguration; } + + return default; } } diff --git a/src/Nexus/Services/DataService.cs b/src/Nexus/Services/DataService.cs index ee7b2a5b..4d90c1d8 100644 --- a/src/Nexus/Services/DataService.cs +++ b/src/Nexus/Services/DataService.cs @@ -6,396 +6,395 @@ using Nexus.Extensibility; using Nexus.Utilities; -namespace Nexus.Services +namespace Nexus.Services; + +internal interface IDataService +{ + Progress ReadProgress { get; } + Progress WriteProgress { get; } + + Task ReadAsStreamAsync( + string resourcePath, + DateTime begin, + DateTime end, + CancellationToken cancellationToken); + + Task ReadAsDoubleAsync( + string resourcePath, + DateTime begin, + DateTime end, + Memory buffer, + CancellationToken cancellationToken); + + Task ExportAsync( + Guid exportId, + IEnumerable catalogItemRequests, + ReadDataHandler readDataHandler, + ExportParameters exportParameters, + CancellationToken cancellationToken); +} + +internal class DataService : IDataService { - internal interface IDataService + #region Fields + + private readonly AppState _appState; + private readonly IMemoryTracker _memoryTracker; + private readonly ClaimsPrincipal _user; + private readonly ILogger _logger; + private readonly ILoggerFactory _loggerFactory; + private readonly IDatabaseService _databaseService; + private readonly IDataControllerService _dataControllerService; + + #endregion + + #region Constructors + + public DataService( + AppState appState, + ClaimsPrincipal user, + IDataControllerService dataControllerService, + IDatabaseService databaseService, + IMemoryTracker memoryTracker, + ILogger logger, + ILoggerFactory loggerFactory) { - Progress ReadProgress { get; } - Progress WriteProgress { get; } - - Task ReadAsStreamAsync( - string resourcePath, - DateTime begin, - DateTime end, - CancellationToken cancellationToken); - - Task ReadAsDoubleAsync( - string resourcePath, - DateTime begin, - DateTime end, - Memory buffer, - CancellationToken cancellationToken); - - Task ExportAsync( - Guid exportId, - IEnumerable catalogItemRequests, - ReadDataHandler readDataHandler, - ExportParameters exportParameters, - CancellationToken cancellationToken); + _user = user; + _appState = appState; + _dataControllerService = dataControllerService; + _databaseService = databaseService; + _memoryTracker = memoryTracker; + _logger = logger; + _loggerFactory = loggerFactory; + + ReadProgress = new Progress(); + WriteProgress = new Progress(); } - internal class DataService : IDataService - { - #region Fields - - private readonly AppState _appState; - private readonly IMemoryTracker _memoryTracker; - private readonly ClaimsPrincipal _user; - private readonly ILogger _logger; - private readonly ILoggerFactory _loggerFactory; - private readonly IDatabaseService _databaseService; - private readonly IDataControllerService _dataControllerService; - - #endregion - - #region Constructors - - public DataService( - AppState appState, - ClaimsPrincipal user, - IDataControllerService dataControllerService, - IDatabaseService databaseService, - IMemoryTracker memoryTracker, - ILogger logger, - ILoggerFactory loggerFactory) - { - _user = user; - _appState = appState; - _dataControllerService = dataControllerService; - _databaseService = databaseService; - _memoryTracker = memoryTracker; - _logger = logger; - _loggerFactory = loggerFactory; - - ReadProgress = new Progress(); - WriteProgress = new Progress(); - } + #endregion + + #region Properties + + public Progress ReadProgress { get; } - #endregion + public Progress WriteProgress { get; } - #region Properties + #endregion - public Progress ReadProgress { get; } + #region Methods + + public async Task ReadAsStreamAsync( + string resourcePath, + DateTime begin, + DateTime end, + CancellationToken cancellationToken) + { + begin = DateTime.SpecifyKind(begin, DateTimeKind.Utc); + end = DateTime.SpecifyKind(end, DateTimeKind.Utc); + + // find representation + var root = _appState.CatalogState.Root; + var catalogItemRequest = await root.TryFindAsync(resourcePath, cancellationToken); + + if (catalogItemRequest is null) + throw new Exception($"Could not find resource path {resourcePath}."); + + var catalogContainer = catalogItemRequest.Container; + + // security check + if (!AuthUtilities.IsCatalogReadable(catalogContainer.Id, catalogContainer.Metadata, catalogContainer.Owner, _user)) + throw new Exception($"The current user is not permitted to access the catalog {catalogContainer.Id}."); + + // controller + + /* IMPORTANT: controller cannot be disposed here because it needs to + * stay alive until the stream has finished. Therefore it will be dipose + * in the DataSourceControllerExtensions.ReadAsStream method which monitors that. + */ + var controller = await _dataControllerService.GetDataSourceControllerAsync( + catalogContainer.DataSourceRegistration, + cancellationToken); + + // read data + var stream = controller.ReadAsStream( + begin, + end, + catalogItemRequest, + readDataHandler: ReadAsDoubleAsync, + _memoryTracker, + _loggerFactory.CreateLogger(), + cancellationToken); + + return stream; + } - public Progress WriteProgress { get; } + public async Task ReadAsDoubleAsync( + string resourcePath, + DateTime begin, + DateTime end, + Memory buffer, + CancellationToken cancellationToken) + { + var stream = await ReadAsStreamAsync( + resourcePath, + begin, + end, + cancellationToken); - #endregion + var byteBuffer = new CastMemoryManager(buffer).Memory; - #region Methods + int bytesRead; - public async Task ReadAsStreamAsync( - string resourcePath, - DateTime begin, - DateTime end, - CancellationToken cancellationToken) + while ((bytesRead = await stream.ReadAsync(byteBuffer, cancellationToken)) > 0) { - begin = DateTime.SpecifyKind(begin, DateTimeKind.Utc); - end = DateTime.SpecifyKind(end, DateTimeKind.Utc); - - // find representation - var root = _appState.CatalogState.Root; - var catalogItemRequest = await root.TryFindAsync(resourcePath, cancellationToken); - - if (catalogItemRequest is null) - throw new Exception($"Could not find resource path {resourcePath}."); - - var catalogContainer = catalogItemRequest.Container; - - // security check - if (!AuthUtilities.IsCatalogReadable(catalogContainer.Id, catalogContainer.Metadata, catalogContainer.Owner, _user)) - throw new Exception($"The current user is not permitted to access the catalog {catalogContainer.Id}."); - - // controller - - /* IMPORTANT: controller cannot be disposed here because it needs to - * stay alive until the stream has finished. Therefore it will be dipose - * in the DataSourceControllerExtensions.ReadAsStream method which monitors that. - */ - var controller = await _dataControllerService.GetDataSourceControllerAsync( - catalogContainer.DataSourceRegistration, - cancellationToken); - - // read data - var stream = controller.ReadAsStream( - begin, - end, - catalogItemRequest, - readDataHandler: ReadAsDoubleAsync, - _memoryTracker, - _loggerFactory.CreateLogger(), - cancellationToken); - - return stream; + byteBuffer = byteBuffer[bytesRead..]; } + } - public async Task ReadAsDoubleAsync( - string resourcePath, - DateTime begin, - DateTime end, - Memory buffer, - CancellationToken cancellationToken) - { - var stream = await ReadAsStreamAsync( - resourcePath, - begin, - end, - cancellationToken); + public async Task ExportAsync( + Guid exportId, + IEnumerable catalogItemRequests, + ReadDataHandler readDataHandler, + ExportParameters exportParameters, + CancellationToken cancellationToken) + { + if (!catalogItemRequests.Any() || exportParameters.Begin == exportParameters.End) + return string.Empty; - var byteBuffer = new CastMemoryManager(buffer).Memory; + // find sample period + var samplePeriods = catalogItemRequests + .Select(catalogItemRequest => catalogItemRequest.Item.Representation.SamplePeriod) + .Distinct() + .ToList(); - int bytesRead; + if (samplePeriods.Count != 1) + throw new ValidationException("All representations must be of the same sample period."); - while ((bytesRead = await stream.ReadAsync(byteBuffer, cancellationToken)) > 0) - { - byteBuffer = byteBuffer[bytesRead..]; - } - } + var samplePeriod = samplePeriods.First(); - public async Task ExportAsync( - Guid exportId, - IEnumerable catalogItemRequests, - ReadDataHandler readDataHandler, - ExportParameters exportParameters, - CancellationToken cancellationToken) - { - if (!catalogItemRequests.Any() || exportParameters.Begin == exportParameters.End) - return string.Empty; + // validate file period + if (exportParameters.FilePeriod.Ticks % samplePeriod.Ticks != 0) + throw new ValidationException("The file period must be a multiple of the sample period."); - // find sample period - var samplePeriods = catalogItemRequests - .Select(catalogItemRequest => catalogItemRequest.Item.Representation.SamplePeriod) - .Distinct() - .ToList(); + // start + string zipFileName = string.Empty; + IDataWriterController? controller = default!; - if (samplePeriods.Count != 1) - throw new ValidationException("All representations must be of the same sample period."); + var tmpFolderPath = Path.Combine(Path.GetTempPath(), "Nexus", Guid.NewGuid().ToString()); - var samplePeriod = samplePeriods.First(); + if (exportParameters.Type is not null) + { + // create tmp/target directory + Directory.CreateDirectory(tmpFolderPath); - // validate file period - if (exportParameters.FilePeriod.Ticks % samplePeriod.Ticks != 0) - throw new ValidationException("The file period must be a multiple of the sample period."); + // copy available licenses + var catalogIds = catalogItemRequests + .Select(request => request.Container.Id) + .Distinct(); - // start - string zipFileName = string.Empty; - IDataWriterController? controller = default!; + foreach (var catalogId in catalogIds) + { + CopyLicenseIfAvailable(catalogId, tmpFolderPath); + } - var tmpFolderPath = Path.Combine(Path.GetTempPath(), "Nexus", Guid.NewGuid().ToString()); + // get data writer controller + var resourceLocator = new Uri(tmpFolderPath, UriKind.Absolute); + controller = await _dataControllerService.GetDataWriterControllerAsync(resourceLocator, exportParameters, cancellationToken); + } - if (exportParameters.Type is not null) - { - // create tmp/target directory - Directory.CreateDirectory(tmpFolderPath); + // write data files + try + { + var exportContext = new ExportContext(samplePeriod, catalogItemRequests, readDataHandler, exportParameters); + await CreateFilesAsync(exportContext, controller, cancellationToken); + } + finally + { + controller?.Dispose(); + } - // copy available licenses - var catalogIds = catalogItemRequests - .Select(request => request.Container.Id) - .Distinct(); + if (exportParameters.Type is not null) + { + // write zip archive + zipFileName = $"{Guid.NewGuid()}.zip"; + var zipArchiveStream = _databaseService.WriteArtifact(zipFileName); + using var zipArchive = new ZipArchive(zipArchiveStream, ZipArchiveMode.Create); + WriteZipArchiveEntries(zipArchive, tmpFolderPath, cancellationToken); + } - foreach (var catalogId in catalogIds) - { - CopyLicenseIfAvailable(catalogId, tmpFolderPath); - } + return zipFileName; + } - // get data writer controller - var resourceLocator = new Uri(tmpFolderPath, UriKind.Absolute); - controller = await _dataControllerService.GetDataWriterControllerAsync(resourceLocator, exportParameters, cancellationToken); - } + private void CopyLicenseIfAvailable(string catalogId, string targetFolder) + { + var enumeratonOptions = new EnumerationOptions() { MatchCasing = MatchCasing.CaseInsensitive }; - // write data files + if (_databaseService.TryReadFirstAttachment(catalogId, "license.md", enumeratonOptions, out var licenseStream)) + { try { - var exportContext = new ExportContext(samplePeriod, catalogItemRequests, readDataHandler, exportParameters); - await CreateFilesAsync(exportContext, controller, cancellationToken); + var prefix = catalogId.TrimStart('/').Replace('/', '_'); + var targetFileName = $"{prefix}_LICENSE.md"; + var targetFile = Path.Combine(targetFolder, targetFileName); + + using var targetFileStream = new FileStream(targetFile, FileMode.OpenOrCreate); + licenseStream.CopyTo(targetFileStream); } finally { - controller?.Dispose(); - } - - if (exportParameters.Type is not null) - { - // write zip archive - zipFileName = $"{Guid.NewGuid()}.zip"; - var zipArchiveStream = _databaseService.WriteArtifact(zipFileName); - using var zipArchive = new ZipArchive(zipArchiveStream, ZipArchiveMode.Create); - WriteZipArchiveEntries(zipArchive, tmpFolderPath, cancellationToken); + licenseStream.Dispose(); } - - return zipFileName; } + } - private void CopyLicenseIfAvailable(string catalogId, string targetFolder) + private async Task CreateFilesAsync( + ExportContext exportContext, + IDataWriterController? dataWriterController, + CancellationToken cancellationToken) + { + /* reading groups */ + var catalogItemRequestPipeReaders = new List(); + var readingGroups = new List(); + + foreach (var group in exportContext.CatalogItemRequests.GroupBy(request => request.Container)) { - var enumeratonOptions = new EnumerationOptions() { MatchCasing = MatchCasing.CaseInsensitive }; + var registration = group.Key.DataSourceRegistration; + var controller = await _dataControllerService.GetDataSourceControllerAsync(registration, cancellationToken); + var catalogItemRequestPipeWriters = new List(); - if (_databaseService.TryReadFirstAttachment(catalogId, "license.md", enumeratonOptions, out var licenseStream)) + foreach (var catalogItemRequest in group) { - try - { - var prefix = catalogId.TrimStart('/').Replace('/', '_'); - var targetFileName = $"{prefix}_LICENSE.md"; - var targetFile = Path.Combine(targetFolder, targetFileName); - - using var targetFileStream = new FileStream(targetFile, FileMode.OpenOrCreate); - licenseStream.CopyTo(targetFileStream); - } - finally - { - licenseStream.Dispose(); - } + var pipe = new Pipe(); + catalogItemRequestPipeWriters.Add(new CatalogItemRequestPipeWriter(catalogItemRequest, pipe.Writer)); + catalogItemRequestPipeReaders.Add(new CatalogItemRequestPipeReader(catalogItemRequest, pipe.Reader)); } + + readingGroups.Add(new DataReadingGroup(controller, catalogItemRequestPipeWriters.ToArray())); } - private async Task CreateFilesAsync( - ExportContext exportContext, - IDataWriterController? dataWriterController, - CancellationToken cancellationToken) + /* cancellation */ + var cts = new CancellationTokenSource(); + cancellationToken.Register(cts.Cancel); + + /* read */ + var exportParameters = exportContext.ExportParameters; + var logger = _loggerFactory.CreateLogger(); + + var reading = DataSourceController.ReadAsync( + exportParameters.Begin, + exportParameters.End, + exportContext.SamplePeriod, + readingGroups.ToArray(), + exportContext.ReadDataHandler, + _memoryTracker, + ReadProgress, + logger, + cts.Token); + + /* write */ + Task writing; + + /* There is not data writer, so just advance through the pipe. */ + if (dataWriterController is null) { - /* reading groups */ - var catalogItemRequestPipeReaders = new List(); - var readingGroups = new List(); - - foreach (var group in exportContext.CatalogItemRequests.GroupBy(request => request.Container)) + var writingTasks = catalogItemRequestPipeReaders.Select(current => { - var registration = group.Key.DataSourceRegistration; - var controller = await _dataControllerService.GetDataSourceControllerAsync(registration, cancellationToken); - var catalogItemRequestPipeWriters = new List(); - - foreach (var catalogItemRequest in group) + return Task.Run(async () => { - var pipe = new Pipe(); - catalogItemRequestPipeWriters.Add(new CatalogItemRequestPipeWriter(catalogItemRequest, pipe.Writer)); - catalogItemRequestPipeReaders.Add(new CatalogItemRequestPipeReader(catalogItemRequest, pipe.Reader)); - } + while (true) + { + var result = await current.DataReader.ReadAsync(cts.Token); - readingGroups.Add(new DataReadingGroup(controller, catalogItemRequestPipeWriters.ToArray())); - } + if (result.IsCompleted) + return; + + else + current.DataReader.AdvanceTo(result.Buffer.End); + } + }, cts.Token); + }); + + writing = Task.WhenAll(writingTasks); + } - /* cancellation */ - var cts = new CancellationTokenSource(); - cancellationToken.Register(cts.Cancel); + /* Normal operation. */ + else + { + var singleFile = exportParameters.FilePeriod == default; - /* read */ - var exportParameters = exportContext.ExportParameters; - var logger = _loggerFactory.CreateLogger(); + var filePeriod = singleFile + ? exportParameters.End - exportParameters.Begin + : exportParameters.FilePeriod; - var reading = DataSourceController.ReadAsync( + writing = dataWriterController.WriteAsync( exportParameters.Begin, exportParameters.End, exportContext.SamplePeriod, - readingGroups.ToArray(), - exportContext.ReadDataHandler, - _memoryTracker, - ReadProgress, - logger, - cts.Token); - - /* write */ - Task writing; - - /* There is not data writer, so just advance through the pipe. */ - if (dataWriterController is null) - { - var writingTasks = catalogItemRequestPipeReaders.Select(current => - { - return Task.Run(async () => - { - while (true) - { - var result = await current.DataReader.ReadAsync(cts.Token); - - if (result.IsCompleted) - return; - - else - current.DataReader.AdvanceTo(result.Buffer.End); - } - }, cts.Token); - }); - - writing = Task.WhenAll(writingTasks); - } - - /* Normal operation. */ - else - { - var singleFile = exportParameters.FilePeriod == default; - - var filePeriod = singleFile - ? exportParameters.End - exportParameters.Begin - : exportParameters.FilePeriod; - - writing = dataWriterController.WriteAsync( - exportParameters.Begin, - exportParameters.End, - exportContext.SamplePeriod, - filePeriod, - catalogItemRequestPipeReaders.ToArray(), - WriteProgress, - cts.Token - ); - } + filePeriod, + catalogItemRequestPipeReaders.ToArray(), + WriteProgress, + cts.Token + ); + } - var tasks = new List() { reading, writing }; + var tasks = new List() { reading, writing }; - try - { - await NexusUtilities.WhenAllFailFastAsync(tasks, cts.Token); - } - catch - { - await cts.CancelAsync(); - throw; - } + try + { + await NexusUtilities.WhenAllFailFastAsync(tasks, cts.Token); } + catch + { + await cts.CancelAsync(); + throw; + } + } - private void WriteZipArchiveEntries(ZipArchive zipArchive, string sourceFolderPath, CancellationToken cancellationToken) + private void WriteZipArchiveEntries(ZipArchive zipArchive, string sourceFolderPath, CancellationToken cancellationToken) + { + ((IProgress)WriteProgress).Report(0); + + try { - ((IProgress)WriteProgress).Report(0); + // write zip archive entries + var filePaths = Directory.GetFiles(sourceFolderPath, "*", SearchOption.AllDirectories); + var fileCount = filePaths.Length; + var currentCount = 0; - try + foreach (string filePath in filePaths) { - // write zip archive entries - var filePaths = Directory.GetFiles(sourceFolderPath, "*", SearchOption.AllDirectories); - var fileCount = filePaths.Length; - var currentCount = 0; + cancellationToken.ThrowIfCancellationRequested(); - foreach (string filePath in filePaths) - { - cancellationToken.ThrowIfCancellationRequested(); - - _logger.LogTrace("Write content of {FilePath} to the ZIP archive", filePath); + _logger.LogTrace("Write content of {FilePath} to the ZIP archive", filePath); - var zipArchiveEntry = zipArchive.CreateEntry(Path.GetFileName(filePath), CompressionLevel.Optimal); + var zipArchiveEntry = zipArchive.CreateEntry(Path.GetFileName(filePath), CompressionLevel.Optimal); - using var fileStream = File.Open(filePath, FileMode.Open, FileAccess.Read); - using var zipArchiveEntryStream = zipArchiveEntry.Open(); + using var fileStream = File.Open(filePath, FileMode.Open, FileAccess.Read); + using var zipArchiveEntryStream = zipArchiveEntry.Open(); - fileStream.CopyTo(zipArchiveEntryStream); + fileStream.CopyTo(zipArchiveEntryStream); - currentCount++; - ((IProgress)WriteProgress).Report(currentCount / (double)fileCount); - } - } - finally - { - CleanUp(sourceFolderPath); + currentCount++; + ((IProgress)WriteProgress).Report(currentCount / (double)fileCount); } } - - private static void CleanUp(string directoryPath) + finally { - try - { - Directory.Delete(directoryPath, true); - } - catch - { - // - } + CleanUp(sourceFolderPath); } + } - #endregion + private static void CleanUp(string directoryPath) + { + try + { + Directory.Delete(directoryPath, true); + } + catch + { + // + } } + + #endregion } diff --git a/src/Nexus/Services/DatabaseService.cs b/src/Nexus/Services/DatabaseService.cs index 666a875a..53ba88dd 100644 --- a/src/Nexus/Services/DatabaseService.cs +++ b/src/Nexus/Services/DatabaseService.cs @@ -3,378 +3,377 @@ using Nexus.DataModel; using System.Diagnostics.CodeAnalysis; -namespace Nexus.Services +namespace Nexus.Services; + +internal interface IDatabaseService +{ + /* /config/catalogs/catalog_id.json */ + bool TryReadCatalogMetadata(string catalogId, [NotNullWhen(true)] out string? catalogMetadata); + Stream WriteCatalogMetadata(string catalogId); + + /* /config/project.json */ + bool TryReadProject([NotNullWhen(true)] out string? project); + Stream WriteProject(); + + /* /catalogs/catalog_id/... */ + bool AttachmentExists(string catalogId, string attachmentId); + IEnumerable EnumerateAttachments(string catalogId); + bool TryReadAttachment(string catalogId, string attachmentId, [NotNullWhen(true)] out Stream? attachment); + bool TryReadFirstAttachment(string catalogId, string searchPattern, EnumerationOptions enumerationOptions, [NotNullWhen(true)] out Stream? attachment); + Stream WriteAttachment(string catalogId, string attachmentId); + void DeleteAttachment(string catalogId, string attachmentId); + + /* /artifacts */ + bool TryReadArtifact(string artifactId, [NotNullWhen(true)] out Stream? artifact); + Stream WriteArtifact(string fileName); + + /* /cache */ + bool TryReadCacheEntry(CatalogItem catalogItem, DateTime begin, [NotNullWhen(true)] out Stream? cacheEntry); + bool TryWriteCacheEntry(CatalogItem catalogItem, DateTime begin, [NotNullWhen(true)] out Stream? cacheEntry); + Task ClearCacheEntriesAsync(string catalogId, DateOnly day, TimeSpan timeout, Predicate predicate); + + /* /users */ + bool TryReadTokenMap( + string userId, + [NotNullWhen(true)] out string? tokenMap); + + Stream WriteTokenMap( + string userId); +} + +internal class DatabaseService : IDatabaseService { - internal interface IDatabaseService + // generated, small files: + // + // /config/catalogs/catalog_id.json + // /config/project.json + // /config/users.db + + // user defined or potentially large files: + // + // /catalogs/catalog_id/... + // /users/user_name/... + // /cache + // /export + // /.nexus/packages + + private readonly PathsOptions _pathsOptions; + + public DatabaseService(IOptions pathsOptions) { - /* /config/catalogs/catalog_id.json */ - bool TryReadCatalogMetadata(string catalogId, [NotNullWhen(true)] out string? catalogMetadata); - Stream WriteCatalogMetadata(string catalogId); - - /* /config/project.json */ - bool TryReadProject([NotNullWhen(true)] out string? project); - Stream WriteProject(); - - /* /catalogs/catalog_id/... */ - bool AttachmentExists(string catalogId, string attachmentId); - IEnumerable EnumerateAttachments(string catalogId); - bool TryReadAttachment(string catalogId, string attachmentId, [NotNullWhen(true)] out Stream? attachment); - bool TryReadFirstAttachment(string catalogId, string searchPattern, EnumerationOptions enumerationOptions, [NotNullWhen(true)] out Stream? attachment); - Stream WriteAttachment(string catalogId, string attachmentId); - void DeleteAttachment(string catalogId, string attachmentId); - - /* /artifacts */ - bool TryReadArtifact(string artifactId, [NotNullWhen(true)] out Stream? artifact); - Stream WriteArtifact(string fileName); - - /* /cache */ - bool TryReadCacheEntry(CatalogItem catalogItem, DateTime begin, [NotNullWhen(true)] out Stream? cacheEntry); - bool TryWriteCacheEntry(CatalogItem catalogItem, DateTime begin, [NotNullWhen(true)] out Stream? cacheEntry); - Task ClearCacheEntriesAsync(string catalogId, DateOnly day, TimeSpan timeout, Predicate predicate); - - /* /users */ - bool TryReadTokenMap( - string userId, - [NotNullWhen(true)] out string? tokenMap); - - Stream WriteTokenMap( - string userId); + _pathsOptions = pathsOptions.Value; } - internal class DatabaseService : IDatabaseService + /* /config/catalogs/catalog_id.json */ + public bool TryReadCatalogMetadata(string catalogId, [NotNullWhen(true)] out string? catalogMetadata) { - // generated, small files: - // - // /config/catalogs/catalog_id.json - // /config/project.json - // /config/users.db - - // user defined or potentially large files: - // - // /catalogs/catalog_id/... - // /users/user_name/... - // /cache - // /export - // /.nexus/packages - - private readonly PathsOptions _pathsOptions; - - public DatabaseService(IOptions pathsOptions) + var physicalId = catalogId.TrimStart('/').Replace("/", "_"); + var catalogMetadataFileName = $"{physicalId}.json"; + var filePath = SafePathCombine(_pathsOptions.Config, Path.Combine("catalogs", catalogMetadataFileName)); + + catalogMetadata = default; + + if (File.Exists(filePath)) { - _pathsOptions = pathsOptions.Value; + catalogMetadata = File.ReadAllText(filePath); + return true; } - /* /config/catalogs/catalog_id.json */ - public bool TryReadCatalogMetadata(string catalogId, [NotNullWhen(true)] out string? catalogMetadata) - { - var physicalId = catalogId.TrimStart('/').Replace("/", "_"); - var catalogMetadataFileName = $"{physicalId}.json"; - var filePath = SafePathCombine(_pathsOptions.Config, Path.Combine("catalogs", catalogMetadataFileName)); + return false; + } - catalogMetadata = default; + public Stream WriteCatalogMetadata(string catalogId) + { + var physicalId = catalogId.TrimStart('/').Replace("/", "_"); + var catalogMetadataFileName = $"{physicalId}.json"; + var folderPath = Path.Combine(_pathsOptions.Config, "catalogs"); - if (File.Exists(filePath)) - { - catalogMetadata = File.ReadAllText(filePath); - return true; - } + Directory.CreateDirectory(folderPath); - return false; - } + var filePath = SafePathCombine(folderPath, catalogMetadataFileName); - public Stream WriteCatalogMetadata(string catalogId) + return File.Open(filePath, FileMode.Create, FileAccess.Write); + } + + /* /config/project.json */ + public bool TryReadProject([NotNullWhen(true)] out string? project) + { + var filePath = Path.Combine(_pathsOptions.Config, "project.json"); + project = default; + + if (File.Exists(filePath)) { - var physicalId = catalogId.TrimStart('/').Replace("/", "_"); - var catalogMetadataFileName = $"{physicalId}.json"; - var folderPath = Path.Combine(_pathsOptions.Config, "catalogs"); + project = File.ReadAllText(filePath); + return true; + } - Directory.CreateDirectory(folderPath); + return false; + } - var filePath = SafePathCombine(folderPath, catalogMetadataFileName); + public Stream WriteProject() + { + Directory.CreateDirectory(_pathsOptions.Config); - return File.Open(filePath, FileMode.Create, FileAccess.Write); - } + var filePath = Path.Combine(_pathsOptions.Config, "project.json"); + return File.Open(filePath, FileMode.Create, FileAccess.Write); + } - /* /config/project.json */ - public bool TryReadProject([NotNullWhen(true)] out string? project) - { - var filePath = Path.Combine(_pathsOptions.Config, "project.json"); - project = default; + /* /catalogs/catalog_id/... */ - if (File.Exists(filePath)) - { - project = File.ReadAllText(filePath); - return true; - } + public bool AttachmentExists(string catalogId, string attachmentId) + { + var physicalId = catalogId.TrimStart('/').Replace("/", "_"); + var attachmentFile = SafePathCombine(Path.Combine(_pathsOptions.Catalogs, physicalId), attachmentId); - return false; - } + return File.Exists(attachmentFile); + } - public Stream WriteProject() - { - Directory.CreateDirectory(_pathsOptions.Config); + public IEnumerable EnumerateAttachments(string catalogId) + { + var physicalId = catalogId.TrimStart('/').Replace("/", "_"); + var attachmentFolder = SafePathCombine(_pathsOptions.Catalogs, physicalId); - var filePath = Path.Combine(_pathsOptions.Config, "project.json"); - return File.Open(filePath, FileMode.Create, FileAccess.Write); - } + if (Directory.Exists(attachmentFolder)) + return Directory + .EnumerateFiles(attachmentFolder, "*", SearchOption.AllDirectories) + .Select(attachmentFilePath => attachmentFilePath[(attachmentFolder.Length + 1)..]); - /* /catalogs/catalog_id/... */ + else + return Enumerable.Empty(); + } - public bool AttachmentExists(string catalogId, string attachmentId) + public bool TryReadAttachment(string catalogId, string attachmentId, [NotNullWhen(true)] out Stream? attachment) + { + attachment = default; + + var physicalId = catalogId.TrimStart('/').Replace("/", "_"); + var attachmentFolder = Path.Combine(_pathsOptions.Catalogs, physicalId); + + if (Directory.Exists(attachmentFolder)) { - var physicalId = catalogId.TrimStart('/').Replace("/", "_"); - var attachmentFile = SafePathCombine(Path.Combine(_pathsOptions.Catalogs, physicalId), attachmentId); + var attachmentFile = SafePathCombine(attachmentFolder, attachmentId); - return File.Exists(attachmentFile); + if (File.Exists(attachmentFile)) + { + attachment = File.OpenRead(attachmentFile); + return true; + } } - public IEnumerable EnumerateAttachments(string catalogId) - { - var physicalId = catalogId.TrimStart('/').Replace("/", "_"); - var attachmentFolder = SafePathCombine(_pathsOptions.Catalogs, physicalId); + return false; + } - if (Directory.Exists(attachmentFolder)) - return Directory - .EnumerateFiles(attachmentFolder, "*", SearchOption.AllDirectories) - .Select(attachmentFilePath => attachmentFilePath[(attachmentFolder.Length + 1)..]); + public bool TryReadFirstAttachment(string catalogId, string searchPattern, EnumerationOptions enumerationOptions, [NotNullWhen(true)] out Stream? attachment) + { + attachment = default; - else - return Enumerable.Empty(); - } + var physicalId = catalogId.TrimStart('/').Replace("/", "_"); + var attachmentFolder = SafePathCombine(_pathsOptions.Catalogs, physicalId); - public bool TryReadAttachment(string catalogId, string attachmentId, [NotNullWhen(true)] out Stream? attachment) + if (Directory.Exists(attachmentFolder)) { - attachment = default; + var attachmentFile = Directory + .EnumerateFiles(attachmentFolder, searchPattern, enumerationOptions) + .FirstOrDefault(); - var physicalId = catalogId.TrimStart('/').Replace("/", "_"); - var attachmentFolder = Path.Combine(_pathsOptions.Catalogs, physicalId); - - if (Directory.Exists(attachmentFolder)) + if (attachmentFile is not null) { - var attachmentFile = SafePathCombine(attachmentFolder, attachmentId); - - if (File.Exists(attachmentFile)) - { - attachment = File.OpenRead(attachmentFile); - return true; - } + attachment = File.OpenRead(attachmentFile); + return true; } - - return false; } - public bool TryReadFirstAttachment(string catalogId, string searchPattern, EnumerationOptions enumerationOptions, [NotNullWhen(true)] out Stream? attachment) - { - attachment = default; + return false; + } - var physicalId = catalogId.TrimStart('/').Replace("/", "_"); - var attachmentFolder = SafePathCombine(_pathsOptions.Catalogs, physicalId); + public Stream WriteAttachment(string catalogId, string attachmentId) + { + var physicalId = catalogId.TrimStart('/').Replace("/", "_"); + var attachmentFile = SafePathCombine(Path.Combine(_pathsOptions.Catalogs, physicalId), attachmentId); + var attachmentFolder = Path.GetDirectoryName(attachmentFile)!; - if (Directory.Exists(attachmentFolder)) - { - var attachmentFile = Directory - .EnumerateFiles(attachmentFolder, searchPattern, enumerationOptions) - .FirstOrDefault(); + Directory.CreateDirectory(attachmentFolder); - if (attachmentFile is not null) - { - attachment = File.OpenRead(attachmentFile); - return true; - } - } + return File.Open(attachmentFile, FileMode.Create, FileAccess.Write); + } - return false; - } + public void DeleteAttachment(string catalogId, string attachmentId) + { + var physicalId = catalogId.TrimStart('/').Replace("/", "_"); + var attachmentFile = SafePathCombine(Path.Combine(_pathsOptions.Catalogs, physicalId), attachmentId); - public Stream WriteAttachment(string catalogId, string attachmentId) - { - var physicalId = catalogId.TrimStart('/').Replace("/", "_"); - var attachmentFile = SafePathCombine(Path.Combine(_pathsOptions.Catalogs, physicalId), attachmentId); - var attachmentFolder = Path.GetDirectoryName(attachmentFile)!; + File.Delete(attachmentFile); + } - Directory.CreateDirectory(attachmentFolder); + /* /artifact */ + public bool TryReadArtifact(string artifactId, [NotNullWhen(true)] out Stream? artifact) + { + artifact = default; - return File.Open(attachmentFile, FileMode.Create, FileAccess.Write); - } + var attachmentFile = SafePathCombine(_pathsOptions.Artifacts, artifactId); - public void DeleteAttachment(string catalogId, string attachmentId) + if (File.Exists(attachmentFile)) { - var physicalId = catalogId.TrimStart('/').Replace("/", "_"); - var attachmentFile = SafePathCombine(Path.Combine(_pathsOptions.Catalogs, physicalId), attachmentId); - - File.Delete(attachmentFile); + artifact = File.OpenRead(attachmentFile); + return true; } - /* /artifact */ - public bool TryReadArtifact(string artifactId, [NotNullWhen(true)] out Stream? artifact) - { - artifact = default; + return false; + } - var attachmentFile = SafePathCombine(_pathsOptions.Artifacts, artifactId); + public Stream WriteArtifact(string fileName) + { + Directory.CreateDirectory(_pathsOptions.Artifacts); - if (File.Exists(attachmentFile)) - { - artifact = File.OpenRead(attachmentFile); - return true; - } + var filePath = Path.Combine(_pathsOptions.Artifacts, fileName); - return false; - } + return File.Open(filePath, FileMode.Create, FileAccess.Write); + } - public Stream WriteArtifact(string fileName) - { - Directory.CreateDirectory(_pathsOptions.Artifacts); + /* /cache */ + private string GetCacheEntryDirectoryPath(string catalogId, DateOnly day) + => Path.Combine(_pathsOptions.Cache, $"{catalogId.TrimStart('/').Replace("/", "_")}/{day:yyyy-MM}/{day:dd}"); - var filePath = Path.Combine(_pathsOptions.Artifacts, fileName); + private string GetCacheEntryId(CatalogItem catalogItem, DateTime begin) + { + var parametersString = DataModelUtilities.GetRepresentationParameterString(catalogItem.Parameters); + return $"{GetCacheEntryDirectoryPath(catalogItem.Catalog.Id, DateOnly.FromDateTime(begin))}/{begin:yyyy-MM-ddTHH-mm-ss-fffffff}_{catalogItem.Resource.Id}_{catalogItem.Representation.Id}{parametersString}.cache"; + } - return File.Open(filePath, FileMode.Create, FileAccess.Write); - } + public bool TryReadCacheEntry(CatalogItem catalogItem, DateTime begin, [NotNullWhen(true)] out Stream? cacheEntry) + { + cacheEntry = default; - /* /cache */ - private string GetCacheEntryDirectoryPath(string catalogId, DateOnly day) - => Path.Combine(_pathsOptions.Cache, $"{catalogId.TrimStart('/').Replace("/", "_")}/{day:yyyy-MM}/{day:dd}"); + var cacheEntryFilePath = GetCacheEntryId(catalogItem, begin); - private string GetCacheEntryId(CatalogItem catalogItem, DateTime begin) + try { - var parametersString = DataModelUtilities.GetRepresentationParameterString(catalogItem.Parameters); - return $"{GetCacheEntryDirectoryPath(catalogItem.Catalog.Id, DateOnly.FromDateTime(begin))}/{begin:yyyy-MM-ddTHH-mm-ss-fffffff}_{catalogItem.Resource.Id}_{catalogItem.Representation.Id}{parametersString}.cache"; - } + cacheEntry = File.Open(cacheEntryFilePath, FileMode.Open, FileAccess.Read, FileShare.None); + return true; - public bool TryReadCacheEntry(CatalogItem catalogItem, DateTime begin, [NotNullWhen(true)] out Stream? cacheEntry) + } + catch { - cacheEntry = default; - - var cacheEntryFilePath = GetCacheEntryId(catalogItem, begin); - - try - { - cacheEntry = File.Open(cacheEntryFilePath, FileMode.Open, FileAccess.Read, FileShare.None); - return true; - - } - catch - { - return false; - } + return false; } + } - public bool TryWriteCacheEntry(CatalogItem catalogItem, DateTime begin, [NotNullWhen(true)] out Stream? cacheEntry) - { - cacheEntry = default; + public bool TryWriteCacheEntry(CatalogItem catalogItem, DateTime begin, [NotNullWhen(true)] out Stream? cacheEntry) + { + cacheEntry = default; - var cacheEntryFilePath = GetCacheEntryId(catalogItem, begin); - var cacheEntryDirectoryPath = Path.GetDirectoryName(cacheEntryFilePath); + var cacheEntryFilePath = GetCacheEntryId(catalogItem, begin); + var cacheEntryDirectoryPath = Path.GetDirectoryName(cacheEntryFilePath); - if (cacheEntryDirectoryPath is null) - return false; + if (cacheEntryDirectoryPath is null) + return false; - Directory.CreateDirectory(cacheEntryDirectoryPath); + Directory.CreateDirectory(cacheEntryDirectoryPath); - try - { - cacheEntry = File.Open(cacheEntryFilePath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None); - return true; + try + { + cacheEntry = File.Open(cacheEntryFilePath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None); + return true; - } - catch - { - return false; - } } + catch + { + return false; + } + } - public async Task ClearCacheEntriesAsync(string catalogId, DateOnly day, TimeSpan timeout, Predicate predicate) + public async Task ClearCacheEntriesAsync(string catalogId, DateOnly day, TimeSpan timeout, Predicate predicate) + { + var cacheEntryDirectoryPath = GetCacheEntryDirectoryPath(catalogId, day); + + if (Directory.Exists(cacheEntryDirectoryPath)) { - var cacheEntryDirectoryPath = GetCacheEntryDirectoryPath(catalogId, day); + var deleteTasks = new List(); - if (Directory.Exists(cacheEntryDirectoryPath)) + foreach (var cacheEntry in Directory.EnumerateFiles(cacheEntryDirectoryPath)) { - var deleteTasks = new List(); - - foreach (var cacheEntry in Directory.EnumerateFiles(cacheEntryDirectoryPath)) + /* if file should be deleted */ + if (predicate(cacheEntry)) { - /* if file should be deleted */ - if (predicate(cacheEntry)) + /* try direct delete */ + try { - /* try direct delete */ - try - { - File.Delete(cacheEntry); - } - - /* otherwise try asynchronously for a minute */ - catch (IOException) - { - deleteTasks.Add(DeleteCacheEntryAsync(cacheEntry, timeout)); - } + File.Delete(cacheEntry); } - } - await Task.WhenAll(deleteTasks); + /* otherwise try asynchronously for a minute */ + catch (IOException) + { + deleteTasks.Add(DeleteCacheEntryAsync(cacheEntry, timeout)); + } + } } + + await Task.WhenAll(deleteTasks); } + } - private static async Task DeleteCacheEntryAsync(string cacheEntry, TimeSpan timeout) - { - var end = DateTime.UtcNow + timeout; + private static async Task DeleteCacheEntryAsync(string cacheEntry, TimeSpan timeout) + { + var end = DateTime.UtcNow + timeout; - while (DateTime.UtcNow < end) + while (DateTime.UtcNow < end) + { + try { - try - { - File.Delete(cacheEntry); - break; - } - catch (IOException) - { - // file is still in use - } - - await Task.Delay(TimeSpan.FromSeconds(1)); + File.Delete(cacheEntry); + break; + } + catch (IOException) + { + // file is still in use } - if (File.Exists(cacheEntry)) - throw new Exception($"Cannot delete cache entry {cacheEntry}."); + await Task.Delay(TimeSpan.FromSeconds(1)); } - /* /users */ - public bool TryReadTokenMap( - string userId, - [NotNullWhen(true)] out string? tokenMap) - { - var folderPath = SafePathCombine(_pathsOptions.Users, userId); - var tokenFilePath = Path.Combine(folderPath, "tokens.json"); + if (File.Exists(cacheEntry)) + throw new Exception($"Cannot delete cache entry {cacheEntry}."); + } - tokenMap = default; + /* /users */ + public bool TryReadTokenMap( + string userId, + [NotNullWhen(true)] out string? tokenMap) + { + var folderPath = SafePathCombine(_pathsOptions.Users, userId); + var tokenFilePath = Path.Combine(folderPath, "tokens.json"); - if (File.Exists(tokenFilePath)) - { - tokenMap = File.ReadAllText(tokenFilePath); - return true; - } + tokenMap = default; - return false; + if (File.Exists(tokenFilePath)) + { + tokenMap = File.ReadAllText(tokenFilePath); + return true; } - public Stream WriteTokenMap( - string userId) - { - var folderPath = SafePathCombine(_pathsOptions.Users, userId); - var tokenFilePath = Path.Combine(folderPath, "tokens.json"); + return false; + } - Directory.CreateDirectory(folderPath); + public Stream WriteTokenMap( + string userId) + { + var folderPath = SafePathCombine(_pathsOptions.Users, userId); + var tokenFilePath = Path.Combine(folderPath, "tokens.json"); - return File.Open(tokenFilePath, FileMode.Create, FileAccess.Write); - } + Directory.CreateDirectory(folderPath); - // - private static string SafePathCombine(string basePath, string relativePath) - { - var filePath = Path.GetFullPath(Path.Combine(basePath, relativePath)); + return File.Open(tokenFilePath, FileMode.Create, FileAccess.Write); + } + + // + private static string SafePathCombine(string basePath, string relativePath) + { + var filePath = Path.GetFullPath(Path.Combine(basePath, relativePath)); - if (!filePath.StartsWith(basePath)) - throw new Exception("Invalid path."); + if (!filePath.StartsWith(basePath)) + throw new Exception("Invalid path."); - return filePath; - } + return filePath; } } \ No newline at end of file diff --git a/src/Nexus/Services/DbService.cs b/src/Nexus/Services/DbService.cs index 0131bb26..61007328 100644 --- a/src/Nexus/Services/DbService.cs +++ b/src/Nexus/Services/DbService.cs @@ -1,104 +1,103 @@ using Microsoft.EntityFrameworkCore; using Nexus.Core; -namespace Nexus.Services +namespace Nexus.Services; + +internal interface IDBService { - internal interface IDBService + IQueryable GetUsers(); + Task FindUserAsync(string userId); + Task FindClaimAsync(Guid claimId); + Task AddOrUpdateUserAsync(NexusUser user); + Task AddOrUpdateClaimAsync(NexusClaim claim); + Task DeleteUserAsync(string userId); + Task SaveChangesAsync(); +} + +internal class DbService : IDBService +{ + private readonly UserDbContext _context; + + public DbService( + UserDbContext context) { - IQueryable GetUsers(); - Task FindUserAsync(string userId); - Task FindClaimAsync(Guid claimId); - Task AddOrUpdateUserAsync(NexusUser user); - Task AddOrUpdateClaimAsync(NexusClaim claim); - Task DeleteUserAsync(string userId); - Task SaveChangesAsync(); + _context = context; + } + + public IQueryable GetUsers() + { + return _context.Users; + } + + public Task FindUserAsync(string userId) + { + return _context.Users + .Include(user => user.Claims) + .AsSingleQuery() + .FirstOrDefaultAsync(user => user.Id == userId); + + /* .AsSingleQuery() avoids the following: + * + * WRN: Microsoft.EntityFrameworkCore.Query + * Compiling a query which loads related collections for more + * than one collection navigation, either via 'Include' or through + * projection, but no 'QuerySplittingBehavior' has been configured. + * By default, Entity Framework will use 'QuerySplittingBehavior.SingleQuery', + * which can potentially result in slow query performance. See + * https:*go.microsoft.com/fwlink/?linkid=2134277 for more information. + * To identify the query that's triggering this warning call + * 'ConfigureWarnings(w => w.Throw(RelationalEventId.MultipleCollectionIncludeWarning))'. + */ + + } + + public async Task FindClaimAsync(Guid claimId) + { + var claim = await _context.Claims + .Include(claim => claim.Owner) + .FirstOrDefaultAsync(claim => claim.Id == claimId); + + return claim; + } + + public async Task AddOrUpdateUserAsync(NexusUser user) + { + var reference = await _context.FindAsync(user.Id); + + if (reference is null) + _context.Add(user); + + else // https://stackoverflow.com/a/64094369 + _context.Entry(reference).CurrentValues.SetValues(user); + + await _context.SaveChangesAsync(); + } + + public async Task AddOrUpdateClaimAsync(NexusClaim claim) + { + var reference = await _context.FindAsync(claim.Id); + + if (reference is null) + _context.Add(claim); + + else // https://stackoverflow.com/a/64094369 + _context.Entry(reference).CurrentValues.SetValues(claim); + + await _context.SaveChangesAsync(); + } + + public async Task DeleteUserAsync(string userId) + { + var user = await FindUserAsync(userId); + + if (user is not null) + _context.Users.Remove(user); + + await _context.SaveChangesAsync(); } - internal class DbService : IDBService + public Task SaveChangesAsync() { - private readonly UserDbContext _context; - - public DbService( - UserDbContext context) - { - _context = context; - } - - public IQueryable GetUsers() - { - return _context.Users; - } - - public Task FindUserAsync(string userId) - { - return _context.Users - .Include(user => user.Claims) - .AsSingleQuery() - .FirstOrDefaultAsync(user => user.Id == userId); - - /* .AsSingleQuery() avoids the following: - * - * WRN: Microsoft.EntityFrameworkCore.Query - * Compiling a query which loads related collections for more - * than one collection navigation, either via 'Include' or through - * projection, but no 'QuerySplittingBehavior' has been configured. - * By default, Entity Framework will use 'QuerySplittingBehavior.SingleQuery', - * which can potentially result in slow query performance. See - * https:*go.microsoft.com/fwlink/?linkid=2134277 for more information. - * To identify the query that's triggering this warning call - * 'ConfigureWarnings(w => w.Throw(RelationalEventId.MultipleCollectionIncludeWarning))'. - */ - - } - - public async Task FindClaimAsync(Guid claimId) - { - var claim = await _context.Claims - .Include(claim => claim.Owner) - .FirstOrDefaultAsync(claim => claim.Id == claimId); - - return claim; - } - - public async Task AddOrUpdateUserAsync(NexusUser user) - { - var reference = await _context.FindAsync(user.Id); - - if (reference is null) - _context.Add(user); - - else // https://stackoverflow.com/a/64094369 - _context.Entry(reference).CurrentValues.SetValues(user); - - await _context.SaveChangesAsync(); - } - - public async Task AddOrUpdateClaimAsync(NexusClaim claim) - { - var reference = await _context.FindAsync(claim.Id); - - if (reference is null) - _context.Add(claim); - - else // https://stackoverflow.com/a/64094369 - _context.Entry(reference).CurrentValues.SetValues(claim); - - await _context.SaveChangesAsync(); - } - - public async Task DeleteUserAsync(string userId) - { - var user = await FindUserAsync(userId); - - if (user is not null) - _context.Users.Remove(user); - - await _context.SaveChangesAsync(); - } - - public Task SaveChangesAsync() - { - return _context.SaveChangesAsync(); - } + return _context.SaveChangesAsync(); } } diff --git a/src/Nexus/Services/ExtensionHive.cs b/src/Nexus/Services/ExtensionHive.cs index 0d1a48cf..4ce42eea 100644 --- a/src/Nexus/Services/ExtensionHive.cs +++ b/src/Nexus/Services/ExtensionHive.cs @@ -6,222 +6,221 @@ using System.Diagnostics.CodeAnalysis; using System.Reflection; -namespace Nexus.Services +namespace Nexus.Services; + +internal interface IExtensionHive { - internal interface IExtensionHive - { - IEnumerable GetExtensions( - ) where T : IExtension; + IEnumerable GetExtensions( + ) where T : IExtension; - InternalPackageReference GetPackageReference( - string fullName) where T : IExtension; + InternalPackageReference GetPackageReference( + string fullName) where T : IExtension; - T GetInstance( - string fullName) where T : IExtension; + T GetInstance( + string fullName) where T : IExtension; - Task LoadPackagesAsync( - IEnumerable packageReferences, - IProgress progress, - CancellationToken cancellationToken); + Task LoadPackagesAsync( + IEnumerable packageReferences, + IProgress progress, + CancellationToken cancellationToken); - Task GetVersionsAsync( - InternalPackageReference packageReference, - CancellationToken cancellationToken); - } + Task GetVersionsAsync( + InternalPackageReference packageReference, + CancellationToken cancellationToken); +} - internal class ExtensionHive : IExtensionHive - { - #region Fields +internal class ExtensionHive : IExtensionHive +{ + #region Fields - private readonly ILogger _logger; - private readonly ILoggerFactory _loggerFactory; - private readonly PathsOptions _pathsOptions; + private readonly ILogger _logger; + private readonly ILoggerFactory _loggerFactory; + private readonly PathsOptions _pathsOptions; - private Dictionary>? _packageControllerMap = default!; + private Dictionary>? _packageControllerMap = default!; - #endregion + #endregion - #region Constructors + #region Constructors - public ExtensionHive( - IOptions pathsOptions, - ILogger logger, - ILoggerFactory loggerFactory) - { - _logger = logger; - _loggerFactory = loggerFactory; - _pathsOptions = pathsOptions.Value; - } + public ExtensionHive( + IOptions pathsOptions, + ILogger logger, + ILoggerFactory loggerFactory) + { + _logger = logger; + _loggerFactory = loggerFactory; + _pathsOptions = pathsOptions.Value; + } - #endregion + #endregion - #region Methods + #region Methods - public async Task LoadPackagesAsync( - IEnumerable packageReferences, - IProgress progress, - CancellationToken cancellationToken) + public async Task LoadPackagesAsync( + IEnumerable packageReferences, + IProgress progress, + CancellationToken cancellationToken) + { + // clean up + if (_packageControllerMap is not null) { - // clean up - if (_packageControllerMap is not null) - { - _logger.LogDebug("Unload previously loaded packages"); + _logger.LogDebug("Unload previously loaded packages"); - foreach (var (controller, _) in _packageControllerMap) - { - controller.Unload(); - } - - _packageControllerMap = default; + foreach (var (controller, _) in _packageControllerMap) + { + controller.Unload(); } - var nexusPackageReference = new InternalPackageReference( - Id: PackageController.BUILTIN_ID, - Provider: PackageController.BUILTIN_PROVIDER, - Configuration: new Dictionary() - ); + _packageControllerMap = default; + } - packageReferences = new List() { nexusPackageReference }.Concat(packageReferences); + var nexusPackageReference = new InternalPackageReference( + Id: PackageController.BUILTIN_ID, + Provider: PackageController.BUILTIN_PROVIDER, + Configuration: new Dictionary() + ); - // build new - var packageControllerMap = new Dictionary>(); - var currentCount = 0; - var totalCount = packageReferences.Count(); + packageReferences = new List() { nexusPackageReference }.Concat(packageReferences); - foreach (var packageReference in packageReferences) - { - var packageController = new PackageController(packageReference, _loggerFactory.CreateLogger()); - using var scope = _logger.BeginScope(packageReference.Configuration.ToDictionary(entry => entry.Key, entry => (object)entry.Value)); + // build new + var packageControllerMap = new Dictionary>(); + var currentCount = 0; + var totalCount = packageReferences.Count(); - try - { - _logger.LogDebug("Load package"); - var assembly = await packageController.LoadAsync(_pathsOptions.Packages, cancellationToken); - - /* Currently, only the directly referenced assembly is being searched for extensions. When this - * behavior should change, it is important to think about the consequences: What should happen when - * an extension is references as usual but at the same time it serves as a base class extensions in - * other packages. If all assemblies in that package are being scanned, the original extension would - * be found twice. - */ - var types = ScanAssembly(assembly, packageReference.Provider == PackageController.BUILTIN_PROVIDER - ? assembly.DefinedTypes - : assembly.ExportedTypes); - - packageControllerMap[packageController] = types; - } - catch (Exception ex) - { - _logger.LogError(ex, "Loading package failed"); - } + foreach (var packageReference in packageReferences) + { + var packageController = new PackageController(packageReference, _loggerFactory.CreateLogger()); + using var scope = _logger.BeginScope(packageReference.Configuration.ToDictionary(entry => entry.Key, entry => (object)entry.Value)); - currentCount++; - progress.Report(currentCount / (double)totalCount); + try + { + _logger.LogDebug("Load package"); + var assembly = await packageController.LoadAsync(_pathsOptions.Packages, cancellationToken); + + /* Currently, only the directly referenced assembly is being searched for extensions. When this + * behavior should change, it is important to think about the consequences: What should happen when + * an extension is references as usual but at the same time it serves as a base class extensions in + * other packages. If all assemblies in that package are being scanned, the original extension would + * be found twice. + */ + var types = ScanAssembly(assembly, packageReference.Provider == PackageController.BUILTIN_PROVIDER + ? assembly.DefinedTypes + : assembly.ExportedTypes); + + packageControllerMap[packageController] = types; + } + catch (Exception ex) + { + _logger.LogError(ex, "Loading package failed"); } - _packageControllerMap = packageControllerMap; + currentCount++; + progress.Report(currentCount / (double)totalCount); } - public Task GetVersionsAsync( - InternalPackageReference packageReference, - CancellationToken cancellationToken) - { - var controller = new PackageController( - packageReference, - _loggerFactory.CreateLogger()); - - return controller.DiscoverAsync(cancellationToken); - } + _packageControllerMap = packageControllerMap; + } - public IEnumerable GetExtensions() where T : IExtension - { - if (_packageControllerMap is null) - { - return Enumerable.Empty(); - } + public Task GetVersionsAsync( + InternalPackageReference packageReference, + CancellationToken cancellationToken) + { + var controller = new PackageController( + packageReference, + _loggerFactory.CreateLogger()); - else - { - var types = _packageControllerMap.SelectMany(entry => entry.Value); + return controller.DiscoverAsync(cancellationToken); + } - return types - .Where(type => typeof(T).IsAssignableFrom(type)); - } + public IEnumerable GetExtensions() where T : IExtension + { + if (_packageControllerMap is null) + { + return Enumerable.Empty(); } - public InternalPackageReference GetPackageReference(string fullName) where T : IExtension + else { - if (!TryGetTypeInfo(fullName, out var packageController, out var _)) - throw new Exception($"Could not find extension {fullName} of type {typeof(T).FullName}."); + var types = _packageControllerMap.SelectMany(entry => entry.Value); - return packageController.PackageReference; + return types + .Where(type => typeof(T).IsAssignableFrom(type)); } + } - public T GetInstance(string fullName) where T : IExtension - { - if (!TryGetTypeInfo(fullName, out var _, out var type)) - throw new Exception($"Could not find extension {fullName} of type {typeof(T).FullName}."); + public InternalPackageReference GetPackageReference(string fullName) where T : IExtension + { + if (!TryGetTypeInfo(fullName, out var packageController, out var _)) + throw new Exception($"Could not find extension {fullName} of type {typeof(T).FullName}."); - _logger.LogDebug("Instantiate extension {ExtensionType}", fullName); + return packageController.PackageReference; + } - var instance = (T)(Activator.CreateInstance(type) ?? throw new Exception("instance is null")); + public T GetInstance(string fullName) where T : IExtension + { + if (!TryGetTypeInfo(fullName, out var _, out var type)) + throw new Exception($"Could not find extension {fullName} of type {typeof(T).FullName}."); - return instance; - } + _logger.LogDebug("Instantiate extension {ExtensionType}", fullName); - private bool TryGetTypeInfo( - string fullName, - [NotNullWhen(true)] out PackageController? packageController, - [NotNullWhen(true)] out Type? type) - where T : IExtension - { - type = default; - packageController = default; + var instance = (T)(Activator.CreateInstance(type) ?? throw new Exception("instance is null")); - if (_packageControllerMap is null) - return false; + return instance; + } - IEnumerable<(PackageController Controller, Type Type)> typeInfos = _packageControllerMap - .SelectMany(entry => entry.Value.Select(type => (entry.Key, type))); + private bool TryGetTypeInfo( + string fullName, + [NotNullWhen(true)] out PackageController? packageController, + [NotNullWhen(true)] out Type? type) + where T : IExtension + { + type = default; + packageController = default; - (packageController, type) = typeInfos - .Where(typeInfo => typeof(T).IsAssignableFrom(typeInfo.Type) && typeInfo.Type.FullName == fullName) - .FirstOrDefault(); + if (_packageControllerMap is null) + return false; - if (type is null) - return false; + IEnumerable<(PackageController Controller, Type Type)> typeInfos = _packageControllerMap + .SelectMany(entry => entry.Value.Select(type => (entry.Key, type))); - return true; - } + (packageController, type) = typeInfos + .Where(typeInfo => typeof(T).IsAssignableFrom(typeInfo.Type) && typeInfo.Type.FullName == fullName) + .FirstOrDefault(); - private ReadOnlyCollection ScanAssembly(Assembly assembly, IEnumerable types) - { - var foundTypes = types - .Where(type => - { - var isClass = type.IsClass; - var isInstantiatable = !type.IsAbstract; - var isDataSource = typeof(IDataSource).IsAssignableFrom(type); - var isDataWriter = typeof(IDataWriter).IsAssignableFrom(type); + if (type is null) + return false; - if (isClass && isInstantiatable && (isDataSource | isDataWriter)) - { - var hasParameterlessConstructor = type.GetConstructor(Type.EmptyTypes) is not null; + return true; + } - if (!hasParameterlessConstructor) - _logger.LogWarning("Type {TypeName} from assembly {AssemblyName} has no parameterless constructor", type.FullName, assembly.FullName); + private ReadOnlyCollection ScanAssembly(Assembly assembly, IEnumerable types) + { + var foundTypes = types + .Where(type => + { + var isClass = type.IsClass; + var isInstantiatable = !type.IsAbstract; + var isDataSource = typeof(IDataSource).IsAssignableFrom(type); + var isDataWriter = typeof(IDataWriter).IsAssignableFrom(type); - return hasParameterlessConstructor; - } + if (isClass && isInstantiatable && (isDataSource | isDataWriter)) + { + var hasParameterlessConstructor = type.GetConstructor(Type.EmptyTypes) is not null; - return false; - }) - .ToList() - .AsReadOnly(); + if (!hasParameterlessConstructor) + _logger.LogWarning("Type {TypeName} from assembly {AssemblyName} has no parameterless constructor", type.FullName, assembly.FullName); - return foundTypes; - } + return hasParameterlessConstructor; + } - #endregion + return false; + }) + .ToList() + .AsReadOnly(); + + return foundTypes; } + + #endregion } diff --git a/src/Nexus/Services/JobService.cs b/src/Nexus/Services/JobService.cs index d6f3ad62..75b58da7 100644 --- a/src/Nexus/Services/JobService.cs +++ b/src/Nexus/Services/JobService.cs @@ -3,123 +3,122 @@ using System.Diagnostics.CodeAnalysis; using Timer = System.Timers.Timer; -namespace Nexus.Services +namespace Nexus.Services; + +internal interface IJobService { - internal interface IJobService - { - JobControl AddJob( - Job job, - Progress progress, - Func> createTask); + JobControl AddJob( + Job job, + Progress progress, + Func> createTask); - List GetJobs(); + List GetJobs(); - bool TryGetJob( - Guid key, - [NotNullWhen(true)] out JobControl? jobControl); - } + bool TryGetJob( + Guid key, + [NotNullWhen(true)] out JobControl? jobControl); +} - internal class JobService : IJobService - { - #region Fields +internal class JobService : IJobService +{ + #region Fields - private readonly Timer _timer; + private readonly Timer _timer; - private readonly ConcurrentDictionary _jobs = new(); + private readonly ConcurrentDictionary _jobs = new(); - #endregion + #endregion - #region Constructors + #region Constructors - public JobService() + public JobService() + { + _timer = new Timer() { - _timer = new Timer() - { - AutoReset = true, - Enabled = true, - Interval = TimeSpan.FromDays(1).TotalMilliseconds - }; + AutoReset = true, + Enabled = true, + Interval = TimeSpan.FromDays(1).TotalMilliseconds + }; - _timer.Elapsed += (sender, e) => - { - var now = DateTime.UtcNow; - var maxRuntime = TimeSpan.FromDays(3); + _timer.Elapsed += (sender, e) => + { + var now = DateTime.UtcNow; + var maxRuntime = TimeSpan.FromDays(3); - foreach (var jobControl in GetJobs()) + foreach (var jobControl in GetJobs()) + { + if (jobControl.Task.IsCompleted) { - if (jobControl.Task.IsCompleted) - { - var runtime = now - jobControl.Start; + var runtime = now - jobControl.Start; - if (runtime > maxRuntime) - _jobs.TryRemove(jobControl.Job.Id, out _); - } + if (runtime > maxRuntime) + _jobs.TryRemove(jobControl.Job.Id, out _); } - }; - } + } + }; + } + + #endregion + + #region Methods - #endregion + public JobControl AddJob( + Job job, + Progress progress, + Func> createTask) + { + var cancellationTokenSource = new CancellationTokenSource(); - #region Methods + var jobControl = new JobControl( + Start: DateTime.UtcNow, + Job: job, + CancellationTokenSource: cancellationTokenSource); - public JobControl AddJob( - Job job, - Progress progress, - Func> createTask) + void progressHandler(object? sender, double e) { - var cancellationTokenSource = new CancellationTokenSource(); + jobControl.OnProgressUpdated(e); + } - var jobControl = new JobControl( - Start: DateTime.UtcNow, - Job: job, - CancellationTokenSource: cancellationTokenSource); + progress.ProgressChanged += progressHandler; + jobControl.Task = createTask(jobControl, cancellationTokenSource); - void progressHandler(object? sender, double e) + _ = Task.Run(async () => + { + try { - jobControl.OnProgressUpdated(e); + await jobControl.Task; } - - progress.ProgressChanged += progressHandler; - jobControl.Task = createTask(jobControl, cancellationTokenSource); - - _ = Task.Run(async () => + finally { - try - { - await jobControl.Task; - } - finally - { - jobControl.OnCompleted(); - jobControl.ProgressUpdated -= progressHandler; - } - }); - - TryAddJob(jobControl); - return jobControl; - } + jobControl.OnCompleted(); + jobControl.ProgressUpdated -= progressHandler; + } + }); - private bool TryAddJob(JobControl jobControl) - { - var result = _jobs.TryAdd(jobControl.Job.Id, jobControl); - return result; - } + TryAddJob(jobControl); + return jobControl; + } - public bool TryGetJob(Guid key, [NotNullWhen(true)] out JobControl? jobControl) - { - return _jobs.TryGetValue(key, out jobControl); - } + private bool TryAddJob(JobControl jobControl) + { + var result = _jobs.TryAdd(jobControl.Job.Id, jobControl); + return result; + } - public List GetJobs() - { - // http://blog.i3arnon.com/2018/01/16/concurrent-dictionary-tolist/ - // https://stackoverflow.com/questions/41038514/calling-tolist-on-concurrentdictionarytkey-tvalue-while-adding-items - return _jobs - .ToArray() - .Select(entry => entry.Value) - .ToList(); - } + public bool TryGetJob(Guid key, [NotNullWhen(true)] out JobControl? jobControl) + { + return _jobs.TryGetValue(key, out jobControl); + } - #endregion + public List GetJobs() + { + // http://blog.i3arnon.com/2018/01/16/concurrent-dictionary-tolist/ + // https://stackoverflow.com/questions/41038514/calling-tolist-on-concurrentdictionarytkey-tvalue-while-adding-items + return _jobs + .ToArray() + .Select(entry => entry.Value) + .ToList(); } + + #endregion } diff --git a/src/Nexus/Services/MemoryTracker.cs b/src/Nexus/Services/MemoryTracker.cs index d2ad038b..8339d4ec 100644 --- a/src/Nexus/Services/MemoryTracker.cs +++ b/src/Nexus/Services/MemoryTracker.cs @@ -1,151 +1,150 @@ using Microsoft.Extensions.Options; using Nexus.Core; -namespace Nexus.Services +namespace Nexus.Services; + +internal class AllocationRegistration : IDisposable { - internal class AllocationRegistration : IDisposable - { - private bool _disposedValue; - private readonly IMemoryTracker _tracker; + private bool _disposedValue; + private readonly IMemoryTracker _tracker; - public AllocationRegistration(IMemoryTracker tracker, long actualByteCount) - { - _tracker = tracker; - ActualByteCount = actualByteCount; - } + public AllocationRegistration(IMemoryTracker tracker, long actualByteCount) + { + _tracker = tracker; + ActualByteCount = actualByteCount; + } - public long ActualByteCount { get; } + public long ActualByteCount { get; } - public void Dispose() + public void Dispose() + { + if (!_disposedValue) { - if (!_disposedValue) - { - _tracker.UnregisterAllocation(this); - _disposedValue = true; - } + _tracker.UnregisterAllocation(this); + _disposedValue = true; } } +} - internal interface IMemoryTracker - { - Task RegisterAllocationAsync(long minimumByteCount, long maximumByteCount, CancellationToken cancellationToken); - void UnregisterAllocation(AllocationRegistration allocationRegistration); - } +internal interface IMemoryTracker +{ + Task RegisterAllocationAsync(long minimumByteCount, long maximumByteCount, CancellationToken cancellationToken); + void UnregisterAllocation(AllocationRegistration allocationRegistration); +} - internal class MemoryTracker : IMemoryTracker - { - private long _consumedBytes; - private readonly DataOptions _dataOptions; - private readonly List _retrySemaphores = new(); - private readonly ILogger _logger; +internal class MemoryTracker : IMemoryTracker +{ + private long _consumedBytes; + private readonly DataOptions _dataOptions; + private readonly List _retrySemaphores = new(); + private readonly ILogger _logger; - public MemoryTracker(IOptions dataOptions, ILogger logger) - { - _dataOptions = dataOptions.Value; - _logger = logger; + public MemoryTracker(IOptions dataOptions, ILogger logger) + { + _dataOptions = dataOptions.Value; + _logger = logger; - _ = Task.Run(MonitorFullGC); - } + _ = Task.Run(MonitorFullGC); + } - internal int Factor { get; set; } = 8; + internal int Factor { get; set; } = 8; - public async Task RegisterAllocationAsync(long minimumByteCount, long maximumByteCount, CancellationToken cancellationToken) - { - if (minimumByteCount > _dataOptions.TotalBufferMemoryConsumption) - throw new Exception("The requested minimum byte count is greater than the total buffer memory consumption parameter."); + public async Task RegisterAllocationAsync(long minimumByteCount, long maximumByteCount, CancellationToken cancellationToken) + { + if (minimumByteCount > _dataOptions.TotalBufferMemoryConsumption) + throw new Exception("The requested minimum byte count is greater than the total buffer memory consumption parameter."); - var myRetrySemaphore = default(SemaphoreSlim); + var myRetrySemaphore = default(SemaphoreSlim); - // loop until registration is successful - while (true) + // loop until registration is successful + while (true) + { + // get exclusive access to _consumedBytes and _retrySemaphores + lock (this) { - // get exclusive access to _consumedBytes and _retrySemaphores - lock (this) - { - var fractionOfRemainingBytes = _consumedBytes >= _dataOptions.TotalBufferMemoryConsumption - ? 0 - : (_dataOptions.TotalBufferMemoryConsumption - _consumedBytes) / Factor /* normal = 8, tests = 2 */; + var fractionOfRemainingBytes = _consumedBytes >= _dataOptions.TotalBufferMemoryConsumption + ? 0 + : (_dataOptions.TotalBufferMemoryConsumption - _consumedBytes) / Factor /* normal = 8, tests = 2 */; - long actualByteCount = 0; + long actualByteCount = 0; - if (fractionOfRemainingBytes >= maximumByteCount) - actualByteCount = maximumByteCount; + if (fractionOfRemainingBytes >= maximumByteCount) + actualByteCount = maximumByteCount; - else if (fractionOfRemainingBytes >= minimumByteCount) - actualByteCount = fractionOfRemainingBytes; + else if (fractionOfRemainingBytes >= minimumByteCount) + actualByteCount = fractionOfRemainingBytes; - // success - if (actualByteCount >= minimumByteCount) - { - // remove semaphore from list - if (myRetrySemaphore is not null) - _retrySemaphores.Remove(myRetrySemaphore); + // success + if (actualByteCount >= minimumByteCount) + { + // remove semaphore from list + if (myRetrySemaphore is not null) + _retrySemaphores.Remove(myRetrySemaphore); - _logger.LogTrace("Allocate {ByteCount} bytes ({MegaByteCount} MB)", actualByteCount, actualByteCount / 1024 / 1024); - SetConsumedBytesAndTriggerWaitingTasks(actualByteCount); + _logger.LogTrace("Allocate {ByteCount} bytes ({MegaByteCount} MB)", actualByteCount, actualByteCount / 1024 / 1024); + SetConsumedBytesAndTriggerWaitingTasks(actualByteCount); - return new AllocationRegistration(this, actualByteCount); - } + return new AllocationRegistration(this, actualByteCount); + } - // failure - else + // failure + else + { + // create retry semaphore if not already done + if (myRetrySemaphore is null) { - // create retry semaphore if not already done - if (myRetrySemaphore is null) - { - myRetrySemaphore = new SemaphoreSlim(initialCount: 0, maxCount: 1); - _retrySemaphores.Add(myRetrySemaphore); - } + myRetrySemaphore = new SemaphoreSlim(initialCount: 0, maxCount: 1); + _retrySemaphores.Add(myRetrySemaphore); } } - - // wait until _consumedBytes changes - _logger.LogTrace("Wait until {ByteCount} bytes ({MegaByteCount} MB) are available", minimumByteCount, minimumByteCount / 1024 / 1024); - await myRetrySemaphore.WaitAsync(timeout: TimeSpan.FromMinutes(1), cancellationToken); } + + // wait until _consumedBytes changes + _logger.LogTrace("Wait until {ByteCount} bytes ({MegaByteCount} MB) are available", minimumByteCount, minimumByteCount / 1024 / 1024); + await myRetrySemaphore.WaitAsync(timeout: TimeSpan.FromMinutes(1), cancellationToken); } + } - public void UnregisterAllocation(AllocationRegistration allocationRegistration) + public void UnregisterAllocation(AllocationRegistration allocationRegistration) + { + // get exclusive access to _consumedBytes and _retrySemaphores + lock (this) { - // get exclusive access to _consumedBytes and _retrySemaphores - lock (this) - { - _logger.LogTrace("Release {ByteCount} bytes ({MegaByteCount} MB)", allocationRegistration.ActualByteCount, allocationRegistration.ActualByteCount / 1024 / 1024); - SetConsumedBytesAndTriggerWaitingTasks(-allocationRegistration.ActualByteCount); - } + _logger.LogTrace("Release {ByteCount} bytes ({MegaByteCount} MB)", allocationRegistration.ActualByteCount, allocationRegistration.ActualByteCount / 1024 / 1024); + SetConsumedBytesAndTriggerWaitingTasks(-allocationRegistration.ActualByteCount); } + } + + private void SetConsumedBytesAndTriggerWaitingTasks(long difference) + { + _consumedBytes += difference; - private void SetConsumedBytesAndTriggerWaitingTasks(long difference) + // allow all other waiting tasks to continue + foreach (var retrySemaphore in _retrySemaphores) { - _consumedBytes += difference; + if (retrySemaphore.CurrentCount == 0) + retrySemaphore.Release(); + } - // allow all other waiting tasks to continue - foreach (var retrySemaphore in _retrySemaphores) - { - if (retrySemaphore.CurrentCount == 0) - retrySemaphore.Release(); - } + _logger.LogTrace("{ByteCount} bytes ({MegaByteCount} MB) are currently in use", _consumedBytes, _consumedBytes / 1024 / 1024); + } - _logger.LogTrace("{ByteCount} bytes ({MegaByteCount} MB) are currently in use", _consumedBytes, _consumedBytes / 1024 / 1024); - } + private void MonitorFullGC() + { + _logger.LogDebug("Register for full GC notifications"); + GC.RegisterForFullGCNotification(1, 1); - private void MonitorFullGC() + while (true) { - _logger.LogDebug("Register for full GC notifications"); - GC.RegisterForFullGCNotification(1, 1); + var status = GC.WaitForFullGCApproach(); - while (true) - { - var status = GC.WaitForFullGCApproach(); - - if (status == GCNotificationStatus.Succeeded) - _logger.LogDebug("Full GC is approaching"); + if (status == GCNotificationStatus.Succeeded) + _logger.LogDebug("Full GC is approaching"); - status = GC.WaitForFullGCComplete(); + status = GC.WaitForFullGCComplete(); - if (status == GCNotificationStatus.Succeeded) - _logger.LogDebug("Full GC has completed"); - } + if (status == GCNotificationStatus.Succeeded) + _logger.LogDebug("Full GC has completed"); } } } diff --git a/src/Nexus/Utilities/AuthUtilities.cs b/src/Nexus/Utilities/AuthUtilities.cs index fdedb898..829aa5c2 100644 --- a/src/Nexus/Utilities/AuthUtilities.cs +++ b/src/Nexus/Utilities/AuthUtilities.cs @@ -4,167 +4,166 @@ using System.Text.RegularExpressions; using static OpenIddict.Abstractions.OpenIddictConstants; -namespace Nexus.Utilities +namespace Nexus.Utilities; + +internal static class AuthUtilities { - internal static class AuthUtilities + public static string ComponentsToTokenValue(string secret, string userId) { - public static string ComponentsToTokenValue(string secret, string userId) - { - return $"{secret}_{userId}"; - } + return $"{secret}_{userId}"; + } - public static (string userId, string secret) TokenValueToComponents(string tokenValue) - { - var parts = tokenValue.Split('_', count: 2); + public static (string userId, string secret) TokenValueToComponents(string tokenValue) + { + var parts = tokenValue.Split('_', count: 2); - return (parts[1], parts[0]); - } + return (parts[1], parts[0]); + } - public static bool IsCatalogReadable( - string catalogId, - CatalogMetadata catalogMetadata, - ClaimsPrincipal? owner, - ClaimsPrincipal user) - { - return InternalIsCatalogAccessible( - catalogId, - catalogMetadata, - owner, - user, - singleClaimType: NexusClaims.CAN_READ_CATALOG, - groupClaimType: NexusClaims.CAN_READ_CATALOG_GROUP, - checkImplicitAccess: true - ); - } + public static bool IsCatalogReadable( + string catalogId, + CatalogMetadata catalogMetadata, + ClaimsPrincipal? owner, + ClaimsPrincipal user) + { + return InternalIsCatalogAccessible( + catalogId, + catalogMetadata, + owner, + user, + singleClaimType: NexusClaims.CAN_READ_CATALOG, + groupClaimType: NexusClaims.CAN_READ_CATALOG_GROUP, + checkImplicitAccess: true + ); + } - public static bool IsCatalogWritable( - string catalogId, - CatalogMetadata catalogMetadata, - ClaimsPrincipal user) - { - return InternalIsCatalogAccessible( - catalogId, - catalogMetadata, - owner: default, - user, - singleClaimType: NexusClaims.CAN_WRITE_CATALOG, - groupClaimType: NexusClaims.CAN_WRITE_CATALOG_GROUP, - checkImplicitAccess: false - ); - } + public static bool IsCatalogWritable( + string catalogId, + CatalogMetadata catalogMetadata, + ClaimsPrincipal user) + { + return InternalIsCatalogAccessible( + catalogId, + catalogMetadata, + owner: default, + user, + singleClaimType: NexusClaims.CAN_WRITE_CATALOG, + groupClaimType: NexusClaims.CAN_WRITE_CATALOG_GROUP, + checkImplicitAccess: false + ); + } - private static bool InternalIsCatalogAccessible( - string catalogId, - CatalogMetadata catalogMetadata, - ClaimsPrincipal? owner, - ClaimsPrincipal user, - string singleClaimType, - string groupClaimType, - bool checkImplicitAccess) + private static bool InternalIsCatalogAccessible( + string catalogId, + CatalogMetadata catalogMetadata, + ClaimsPrincipal? owner, + ClaimsPrincipal user, + string singleClaimType, + string groupClaimType, + bool checkImplicitAccess) + { + foreach (var identity in user.Identities) { - foreach (var identity in user.Identities) - { - if (identity is null || !identity.IsAuthenticated) - continue; + if (identity is null || !identity.IsAuthenticated) + continue; - if (catalogId == CatalogContainer.RootCatalogId) - return true; - - var implicitAccess = - catalogId == Sample.LocalCatalogId || - catalogId == Sample.RemoteCatalogId; + if (catalogId == CatalogContainer.RootCatalogId) + return true; - if (checkImplicitAccess && implicitAccess) - return true; + var implicitAccess = + catalogId == Sample.LocalCatalogId || + catalogId == Sample.RemoteCatalogId; - var result = false; + if (checkImplicitAccess && implicitAccess) + return true; - /* PAT */ - if (identity.AuthenticationType == PersonalAccessTokenAuthenticationDefaults.AuthenticationScheme) - { - var isAdmin = identity.HasClaim( - NexusClaims.ToPatUserClaimType(Claims.Role), - NexusRoles.ADMINISTRATOR); - - if (isAdmin) - return true; - - /* The token alone can access the catalog ... */ - var canAccessCatalog = identity.HasClaim( - claim => - claim.Type == singleClaimType && - Regex.IsMatch(catalogId, claim.Value) - ); - - /* ... but it cannot be more powerful than the - * user itself, so next step is to ensure that - * the user can access that catalog as well. */ - if (canAccessCatalog) - { - result = CanUserAccessCatalog( - catalogId, - catalogMetadata, - owner, - identity, - NexusClaims.ToPatUserClaimType(singleClaimType), - NexusClaims.ToPatUserClaimType(groupClaimType)); - } - } + var result = false; - /* cookie */ - else - { - var isAdmin = identity.HasClaim( - Claims.Role, - NexusRoles.ADMINISTRATOR); + /* PAT */ + if (identity.AuthenticationType == PersonalAccessTokenAuthenticationDefaults.AuthenticationScheme) + { + var isAdmin = identity.HasClaim( + NexusClaims.ToPatUserClaimType(Claims.Role), + NexusRoles.ADMINISTRATOR); - if (isAdmin) - return true; + if (isAdmin) + return true; - /* ensure that user can read that catalog */ + /* The token alone can access the catalog ... */ + var canAccessCatalog = identity.HasClaim( + claim => + claim.Type == singleClaimType && + Regex.IsMatch(catalogId, claim.Value) + ); + + /* ... but it cannot be more powerful than the + * user itself, so next step is to ensure that + * the user can access that catalog as well. */ + if (canAccessCatalog) + { result = CanUserAccessCatalog( catalogId, catalogMetadata, owner, identity, - singleClaimType, - groupClaimType); + NexusClaims.ToPatUserClaimType(singleClaimType), + NexusClaims.ToPatUserClaimType(groupClaimType)); } + } + + /* cookie */ + else + { + var isAdmin = identity.HasClaim( + Claims.Role, + NexusRoles.ADMINISTRATOR); - /* leave loop when access is granted */ - if (result) + if (isAdmin) return true; + + /* ensure that user can read that catalog */ + result = CanUserAccessCatalog( + catalogId, + catalogMetadata, + owner, + identity, + singleClaimType, + groupClaimType); } - return false; + /* leave loop when access is granted */ + if (result) + return true; } - private static bool CanUserAccessCatalog( - string catalogId, - CatalogMetadata catalogMetadata, - ClaimsPrincipal? owner, - ClaimsIdentity identity, - string singleClaimType, - string groupClaimType - ) - { - var isOwner = - owner is not null && - owner?.FindFirstValue(Claims.Subject) == identity.FindFirst(Claims.Subject)?.Value; - - var canReadCatalog = identity.HasClaim( - claim => - claim.Type == singleClaimType && - Regex.IsMatch(catalogId, claim.Value) - ); - - var canReadCatalogGroup = catalogMetadata.GroupMemberships is not null && identity.HasClaim( - claim => - claim.Type == groupClaimType && - catalogMetadata.GroupMemberships.Any(group => Regex.IsMatch(group, claim.Value)) - ); - - return isOwner || canReadCatalog || canReadCatalogGroup; - } + return false; + } + + private static bool CanUserAccessCatalog( + string catalogId, + CatalogMetadata catalogMetadata, + ClaimsPrincipal? owner, + ClaimsIdentity identity, + string singleClaimType, + string groupClaimType + ) + { + var isOwner = + owner is not null && + owner?.FindFirstValue(Claims.Subject) == identity.FindFirst(Claims.Subject)?.Value; + + var canReadCatalog = identity.HasClaim( + claim => + claim.Type == singleClaimType && + Regex.IsMatch(catalogId, claim.Value) + ); + + var canReadCatalogGroup = catalogMetadata.GroupMemberships is not null && identity.HasClaim( + claim => + claim.Type == groupClaimType && + catalogMetadata.GroupMemberships.Any(group => Regex.IsMatch(group, claim.Value)) + ); + + return isOwner || canReadCatalog || canReadCatalogGroup; } } diff --git a/src/Nexus/Utilities/BufferUtilities.cs b/src/Nexus/Utilities/BufferUtilities.cs index 8f1b99b9..160e6ef9 100644 --- a/src/Nexus/Utilities/BufferUtilities.cs +++ b/src/Nexus/Utilities/BufferUtilities.cs @@ -2,51 +2,50 @@ using Nexus.DataModel; using System.Reflection; -namespace Nexus.Utilities +namespace Nexus.Utilities; + +internal static class BufferUtilities { - internal static class BufferUtilities + public static void ApplyRepresentationStatusByDataType(NexusDataType dataType, ReadOnlyMemory data, ReadOnlyMemory status, Memory target) { - public static void ApplyRepresentationStatusByDataType(NexusDataType dataType, ReadOnlyMemory data, ReadOnlyMemory status, Memory target) - { - var targetType = NexusUtilities.GetTypeFromNexusDataType(dataType); + var targetType = NexusUtilities.GetTypeFromNexusDataType(dataType); - var method = typeof(BufferUtilities) - .GetMethod(nameof(InternalApplyRepresentationStatusByDataType), BindingFlags.NonPublic | BindingFlags.Static)! - .MakeGenericMethod(targetType); + var method = typeof(BufferUtilities) + .GetMethod(nameof(InternalApplyRepresentationStatusByDataType), BindingFlags.NonPublic | BindingFlags.Static)! + .MakeGenericMethod(targetType); - method.Invoke(null, new object[] { data, status, target }); - } + method.Invoke(null, new object[] { data, status, target }); + } - private static void InternalApplyRepresentationStatusByDataType(ReadOnlyMemory data, ReadOnlyMemory status, Memory target) - where T : unmanaged - { - ApplyRepresentationStatus(data.Cast(), status, target); - } + private static void InternalApplyRepresentationStatusByDataType(ReadOnlyMemory data, ReadOnlyMemory status, Memory target) + where T : unmanaged + { + ApplyRepresentationStatus(data.Cast(), status, target); + } - public static unsafe void ApplyRepresentationStatus(ReadOnlyMemory data, ReadOnlyMemory status, Memory target) where T : unmanaged + public static unsafe void ApplyRepresentationStatus(ReadOnlyMemory data, ReadOnlyMemory status, Memory target) where T : unmanaged + { + fixed (T* dataPtr = data.Span) { - fixed (T* dataPtr = data.Span) + fixed (byte* statusPtr = status.Span) { - fixed (byte* statusPtr = status.Span) + fixed (double* targetPtr = target.Span) { - fixed (double* targetPtr = target.Span) - { - InternalApplyRepresentationStatus(target.Length, dataPtr, statusPtr, targetPtr); - } + InternalApplyRepresentationStatus(target.Length, dataPtr, statusPtr, targetPtr); } } } + } - private unsafe static void InternalApplyRepresentationStatus(int length, T* dataPtr, byte* statusPtr, double* targetPtr) where T : unmanaged + private unsafe static void InternalApplyRepresentationStatus(int length, T* dataPtr, byte* statusPtr, double* targetPtr) where T : unmanaged + { + Parallel.For(0, length, i => { - Parallel.For(0, length, i => - { - if (statusPtr[i] != 1) - targetPtr[i] = double.NaN; + if (statusPtr[i] != 1) + targetPtr[i] = double.NaN; - else - targetPtr[i] = GenericToDouble.ToDouble(dataPtr[i]); - }); - } + else + targetPtr[i] = GenericToDouble.ToDouble(dataPtr[i]); + }); } } diff --git a/src/Nexus/Utilities/GenericsUtilities.cs b/src/Nexus/Utilities/GenericsUtilities.cs index f25a3e9e..58ea443d 100644 --- a/src/Nexus/Utilities/GenericsUtilities.cs +++ b/src/Nexus/Utilities/GenericsUtilities.cs @@ -1,70 +1,69 @@ using System.Linq.Expressions; using System.Reflection.Emit; -namespace Nexus.Utilities -{ - internal static class GenericToDouble - { - private static readonly Func _to_double_function = GenericToDouble.EmitToDoubleConverter(); +namespace Nexus.Utilities; - private static Func EmitToDoubleConverter() - { - var method = new DynamicMethod(string.Empty, typeof(double), [typeof(T)]); - var ilGenerator = method.GetILGenerator(); +internal static class GenericToDouble +{ + private static readonly Func _to_double_function = GenericToDouble.EmitToDoubleConverter(); - ilGenerator.Emit(OpCodes.Ldarg_0); + private static Func EmitToDoubleConverter() + { + var method = new DynamicMethod(string.Empty, typeof(double), [typeof(T)]); + var ilGenerator = method.GetILGenerator(); - if (typeof(T) != typeof(double)) - ilGenerator.Emit(OpCodes.Conv_R8); + ilGenerator.Emit(OpCodes.Ldarg_0); - ilGenerator.Emit(OpCodes.Ret); + if (typeof(T) != typeof(double)) + ilGenerator.Emit(OpCodes.Conv_R8); - return (Func)method.CreateDelegate(typeof(Func)); - } + ilGenerator.Emit(OpCodes.Ret); - public static double ToDouble(T value) - { - return _to_double_function(value); - } + return (Func)method.CreateDelegate(typeof(Func)); } - internal static class GenericBitOr + public static double ToDouble(T value) { - private static readonly Func _bit_or_function = GenericBitOr.EmitBitOrFunction(); + return _to_double_function(value); + } +} - private static Func EmitBitOrFunction() - { - var _parameterA = Expression.Parameter(typeof(T), "a"); - var _parameterB = Expression.Parameter(typeof(T), "b"); +internal static class GenericBitOr +{ + private static readonly Func _bit_or_function = GenericBitOr.EmitBitOrFunction(); - var _body = Expression.Or(_parameterA, _parameterB); + private static Func EmitBitOrFunction() + { + var _parameterA = Expression.Parameter(typeof(T), "a"); + var _parameterB = Expression.Parameter(typeof(T), "b"); - return Expression.Lambda>(_body, _parameterA, _parameterB).Compile(); - } + var _body = Expression.Or(_parameterA, _parameterB); - public static T BitOr(T a, T b) - { - return _bit_or_function(a, b); - } + return Expression.Lambda>(_body, _parameterA, _parameterB).Compile(); } - internal static class GenericBitAnd + public static T BitOr(T a, T b) { - private static readonly Func _bit_and_function = GenericBitAnd.EmitBitAndFunction(); + return _bit_or_function(a, b); + } +} + +internal static class GenericBitAnd +{ + private static readonly Func _bit_and_function = GenericBitAnd.EmitBitAndFunction(); - private static Func EmitBitAndFunction() - { - var _parameterA = Expression.Parameter(typeof(T), "a"); - var _parameterB = Expression.Parameter(typeof(T), "b"); + private static Func EmitBitAndFunction() + { + var _parameterA = Expression.Parameter(typeof(T), "a"); + var _parameterB = Expression.Parameter(typeof(T), "b"); - var _body = Expression.And(_parameterA, _parameterB); + var _body = Expression.And(_parameterA, _parameterB); - return Expression.Lambda>(_body, _parameterA, _parameterB).Compile(); - } + return Expression.Lambda>(_body, _parameterA, _parameterB).Compile(); + } - public static T BitAnd(T a, T b) - { - return _bit_and_function(a, b); - } + public static T BitAnd(T a, T b) + { + return _bit_and_function(a, b); } } \ No newline at end of file diff --git a/src/Nexus/Utilities/JsonSerializerHelper.cs b/src/Nexus/Utilities/JsonSerializerHelper.cs index e0761c1e..386ee8cb 100644 --- a/src/Nexus/Utilities/JsonSerializerHelper.cs +++ b/src/Nexus/Utilities/JsonSerializerHelper.cs @@ -2,30 +2,29 @@ using System.Text.Json; using System.Text.Json.Serialization; -namespace Nexus.Utilities +namespace Nexus.Utilities; + +internal static class JsonSerializerHelper { - internal static class JsonSerializerHelper + private static readonly JsonSerializerOptions _options = new() { - private static readonly JsonSerializerOptions _options = new() - { - WriteIndented = true, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault, - Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping - }; + WriteIndented = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }; - public static string SerializeIndented(T value) - { - return JsonSerializer.Serialize(value, _options); - } + public static string SerializeIndented(T value) + { + return JsonSerializer.Serialize(value, _options); + } - public static void SerializeIndented(Stream utf8Json, T value) - { - JsonSerializer.Serialize(utf8Json, value, _options); - } + public static void SerializeIndented(Stream utf8Json, T value) + { + JsonSerializer.Serialize(utf8Json, value, _options); + } - public static Task SerializeIndentedAsync(Stream utf8Json, T value) - { - return JsonSerializer.SerializeAsync(utf8Json, value, _options); - } + public static Task SerializeIndentedAsync(Stream utf8Json, T value) + { + return JsonSerializer.SerializeAsync(utf8Json, value, _options); } } diff --git a/src/Nexus/Utilities/MemoryManager.cs b/src/Nexus/Utilities/MemoryManager.cs index 931f1c29..af2eab65 100644 --- a/src/Nexus/Utilities/MemoryManager.cs +++ b/src/Nexus/Utilities/MemoryManager.cs @@ -1,27 +1,26 @@ using System.Buffers; using System.Runtime.InteropServices; -namespace Nexus.Utilities -{ - // TODO: Validate against this: https://github.com/windows-toolkit/WindowsCommunityToolkit/pull/3520/files - - internal class CastMemoryManager : MemoryManager - where TFrom : struct - where TTo : struct - { - private readonly Memory _from; +namespace Nexus.Utilities; - public CastMemoryManager(Memory from) => _from = from; +// TODO: Validate against this: https://github.com/windows-toolkit/WindowsCommunityToolkit/pull/3520/files - public override Span GetSpan() => MemoryMarshal.Cast(_from.Span); +internal class CastMemoryManager : MemoryManager + where TFrom : struct + where TTo : struct +{ + private readonly Memory _from; - protected override void Dispose(bool disposing) - { - // - } + public CastMemoryManager(Memory from) => _from = from; - public override MemoryHandle Pin(int elementIndex = 0) => throw new NotSupportedException(); + public override Span GetSpan() => MemoryMarshal.Cast(_from.Span); - public override void Unpin() => throw new NotSupportedException(); + protected override void Dispose(bool disposing) + { + // } + + public override MemoryHandle Pin(int elementIndex = 0) => throw new NotSupportedException(); + + public override void Unpin() => throw new NotSupportedException(); } \ No newline at end of file diff --git a/src/Nexus/Utilities/NexusUtilities.cs b/src/Nexus/Utilities/NexusUtilities.cs index 04e85466..f9ca3b9e 100644 --- a/src/Nexus/Utilities/NexusUtilities.cs +++ b/src/Nexus/Utilities/NexusUtilities.cs @@ -4,149 +4,148 @@ using System.Runtime.ExceptionServices; using System.Text.RegularExpressions; -namespace Nexus.Utilities +namespace Nexus.Utilities; + +internal static class NexusUtilities { - internal static class NexusUtilities - { - private static string? _defaultBaseUrl; + private static string? _defaultBaseUrl; - public static string DefaultBaseUrl + public static string DefaultBaseUrl + { + get { - get + if (_defaultBaseUrl is null) { - if (_defaultBaseUrl is null) - { - int port = 5000; - var aspnetcoreEnvVar = Environment.GetEnvironmentVariable("ASPNETCORE_URLS"); - - if (aspnetcoreEnvVar is not null) - { - var match = Regex.Match(aspnetcoreEnvVar, ":([0-9]+)"); + int port = 5000; + var aspnetcoreEnvVar = Environment.GetEnvironmentVariable("ASPNETCORE_URLS"); - if (match.Success && int.TryParse(match.Groups[1].Value, out var parsedPort)) - port = parsedPort; - } + if (aspnetcoreEnvVar is not null) + { + var match = Regex.Match(aspnetcoreEnvVar, ":([0-9]+)"); - _defaultBaseUrl = $"http://localhost:{port}"; + if (match.Success && int.TryParse(match.Groups[1].Value, out var parsedPort)) + port = parsedPort; } - return _defaultBaseUrl; + _defaultBaseUrl = $"http://localhost:{port}"; } + + return _defaultBaseUrl; } + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int Scale(TimeSpan value, TimeSpan samplePeriod) => (int)(value.Ticks / samplePeriod.Ticks); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Scale(TimeSpan value, TimeSpan samplePeriod) => (int)(value.Ticks / samplePeriod.Ticks); - public static List GetEnumValues() where T : Enum - { - return Enum.GetValues(typeof(T)).Cast().ToList(); - } + public static List GetEnumValues() where T : Enum + { + return Enum.GetValues(typeof(T)).Cast().ToList(); + } - public static async Task FileLoopAsync( - DateTime begin, - DateTime end, - TimeSpan filePeriod, - Func func) - { - var lastFileBegin = default(DateTime); - var currentBegin = begin; - var totalPeriod = end - begin; - var remainingPeriod = totalPeriod; + public static async Task FileLoopAsync( + DateTime begin, + DateTime end, + TimeSpan filePeriod, + Func func) + { + var lastFileBegin = default(DateTime); + var currentBegin = begin; + var totalPeriod = end - begin; + var remainingPeriod = totalPeriod; - while (remainingPeriod > TimeSpan.Zero) - { - DateTime fileBegin; + while (remainingPeriod > TimeSpan.Zero) + { + DateTime fileBegin; - if (filePeriod == totalPeriod) - fileBegin = lastFileBegin != DateTime.MinValue ? lastFileBegin : begin; + if (filePeriod == totalPeriod) + fileBegin = lastFileBegin != DateTime.MinValue ? lastFileBegin : begin; - else - fileBegin = currentBegin.RoundDown(filePeriod); + else + fileBegin = currentBegin.RoundDown(filePeriod); - lastFileBegin = fileBegin; + lastFileBegin = fileBegin; - var fileOffset = currentBegin - fileBegin; - var remainingFilePeriod = filePeriod - fileOffset; - var duration = TimeSpan.FromTicks(Math.Min(remainingFilePeriod.Ticks, remainingPeriod.Ticks)); + var fileOffset = currentBegin - fileBegin; + var remainingFilePeriod = filePeriod - fileOffset; + var duration = TimeSpan.FromTicks(Math.Min(remainingFilePeriod.Ticks, remainingPeriod.Ticks)); - await func.Invoke(fileBegin, fileOffset, duration); + await func.Invoke(fileBegin, fileOffset, duration); - // update loop state - currentBegin += duration; - remainingPeriod -= duration; - } + // update loop state + currentBegin += duration; + remainingPeriod -= duration; } + } #pragma warning disable VSTHRD200 // Verwenden Sie das Suffix "Async" für asynchrone Methoden - public static async ValueTask WhenAll(params ValueTask[] tasks) + public static async ValueTask WhenAll(params ValueTask[] tasks) #pragma warning restore VSTHRD200 // Verwenden Sie das Suffix "Async" für asynchrone Methoden - { - List? exceptions = default; + { + List? exceptions = default; - var results = new T[tasks.Length]; + var results = new T[tasks.Length]; - for (var i = 0; i < tasks.Length; i++) + for (var i = 0; i < tasks.Length; i++) + { + try { - try - { - results[i] = await tasks[i]; - } - catch (Exception ex) - { - exceptions ??= new List(tasks.Length); - exceptions.Add(ex); - } + results[i] = await tasks[i]; + } + catch (Exception ex) + { + exceptions ??= new List(tasks.Length); + exceptions.Add(ex); } - - return exceptions is null - ? results - : throw new AggregateException(exceptions); } - public static async Task WhenAllFailFastAsync(List tasks, CancellationToken cancellationToken) + return exceptions is null + ? results + : throw new AggregateException(exceptions); + } + + public static async Task WhenAllFailFastAsync(List tasks, CancellationToken cancellationToken) + { + while (tasks.Any()) { - while (tasks.Any()) - { - var task = await Task - .WhenAny(tasks) - .WaitAsync(cancellationToken); + var task = await Task + .WhenAny(tasks) + .WaitAsync(cancellationToken); - cancellationToken - .ThrowIfCancellationRequested(); + cancellationToken + .ThrowIfCancellationRequested(); - if (task.Exception is not null) - ExceptionDispatchInfo.Capture(task.Exception.InnerException ?? task.Exception).Throw(); + if (task.Exception is not null) + ExceptionDispatchInfo.Capture(task.Exception.InnerException ?? task.Exception).Throw(); - tasks.Remove(task); - } + tasks.Remove(task); } + } - public static Type GetTypeFromNexusDataType(NexusDataType dataType) + public static Type GetTypeFromNexusDataType(NexusDataType dataType) + { + return dataType switch { - return dataType switch - { - NexusDataType.UINT8 => typeof(byte), - NexusDataType.INT8 => typeof(sbyte), - NexusDataType.UINT16 => typeof(ushort), - NexusDataType.INT16 => typeof(short), - NexusDataType.UINT32 => typeof(uint), - NexusDataType.INT32 => typeof(int), - NexusDataType.UINT64 => typeof(ulong), - NexusDataType.INT64 => typeof(long), - NexusDataType.FLOAT32 => typeof(float), - NexusDataType.FLOAT64 => typeof(double), - _ => throw new NotSupportedException($"The specified data type {dataType} is not supported.") - }; - } + NexusDataType.UINT8 => typeof(byte), + NexusDataType.INT8 => typeof(sbyte), + NexusDataType.UINT16 => typeof(ushort), + NexusDataType.INT16 => typeof(short), + NexusDataType.UINT32 => typeof(uint), + NexusDataType.INT32 => typeof(int), + NexusDataType.UINT64 => typeof(ulong), + NexusDataType.INT64 => typeof(long), + NexusDataType.FLOAT32 => typeof(float), + NexusDataType.FLOAT64 => typeof(double), + _ => throw new NotSupportedException($"The specified data type {dataType} is not supported.") + }; + } - public static int SizeOf(NexusDataType dataType) - { - return ((ushort)dataType & 0x00FF) / 8; - } + public static int SizeOf(NexusDataType dataType) + { + return ((ushort)dataType & 0x00FF) / 8; + } - public static IEnumerable GetCustomAttributes(this Type type) where T : Attribute - { - return type.GetCustomAttributes(false).OfType(); - } + public static IEnumerable GetCustomAttributes(this Type type) where T : Attribute + { + return type.GetCustomAttributes(false).OfType(); } } \ No newline at end of file diff --git a/src/extensibility/dotnet-extensibility/DataModel/DataModelExtensions.cs b/src/extensibility/dotnet-extensibility/DataModel/DataModelExtensions.cs index 7adbecf4..c5338556 100644 --- a/src/extensibility/dotnet-extensibility/DataModel/DataModelExtensions.cs +++ b/src/extensibility/dotnet-extensibility/DataModel/DataModelExtensions.cs @@ -1,190 +1,189 @@ using System.Text.Json.Nodes; using System.Text.RegularExpressions; -namespace Nexus.DataModel +namespace Nexus.DataModel; + +/// +/// Contains extension methods to make life easier working with the data model types. +/// +public static class DataModelExtensions { + #region Fluent API + + /// + /// A constant with the key for a readme property. + /// + public const string ReadmeKey = "readme"; + + /// + /// A constant with the key for a license property. + /// + public const string LicenseKey = "license"; + + /// + /// A constant with the key for a description property. + /// + public const string DescriptionKey = "description"; + + /// + /// A constant with the key for a warning property. + /// + public const string WarningKey = "warning"; + + /// + /// A constant with the key for a unit property. + /// + public const string UnitKey = "unit"; + + /// + /// A constant with the key for a groups property. + /// + public const string GroupsKey = "groups"; + + internal const string BasePathKey = "base-path"; + /// - /// Contains extension methods to make life easier working with the data model types. + /// Adds a readme. /// - public static class DataModelExtensions + /// The catalog builder. + /// The markdown readme to add. + /// A resource catalog builder. + public static ResourceCatalogBuilder WithReadme(this ResourceCatalogBuilder catalogBuilder, string readme) { - #region Fluent API - - /// - /// A constant with the key for a readme property. - /// - public const string ReadmeKey = "readme"; - - /// - /// A constant with the key for a license property. - /// - public const string LicenseKey = "license"; - - /// - /// A constant with the key for a description property. - /// - public const string DescriptionKey = "description"; - - /// - /// A constant with the key for a warning property. - /// - public const string WarningKey = "warning"; - - /// - /// A constant with the key for a unit property. - /// - public const string UnitKey = "unit"; - - /// - /// A constant with the key for a groups property. - /// - public const string GroupsKey = "groups"; - - internal const string BasePathKey = "base-path"; - - /// - /// Adds a readme. - /// - /// The catalog builder. - /// The markdown readme to add. - /// A resource catalog builder. - public static ResourceCatalogBuilder WithReadme(this ResourceCatalogBuilder catalogBuilder, string readme) - { - return catalogBuilder.WithProperty(ReadmeKey, readme); - } + return catalogBuilder.WithProperty(ReadmeKey, readme); + } - /// - /// Adds a license. - /// - /// The catalog builder. - /// The markdown license to add. - /// A resource catalog builder. - public static ResourceCatalogBuilder WithLicense(this ResourceCatalogBuilder catalogBuilder, string license) - { - return catalogBuilder.WithProperty(LicenseKey, license); - } + /// + /// Adds a license. + /// + /// The catalog builder. + /// The markdown license to add. + /// A resource catalog builder. + public static ResourceCatalogBuilder WithLicense(this ResourceCatalogBuilder catalogBuilder, string license) + { + return catalogBuilder.WithProperty(LicenseKey, license); + } - /// - /// Adds a unit. - /// - /// The resource builder. - /// The unit to add. - /// A resource builder. - public static ResourceBuilder WithUnit(this ResourceBuilder resourceBuilder, string unit) - { - return resourceBuilder.WithProperty(UnitKey, unit); - } + /// + /// Adds a unit. + /// + /// The resource builder. + /// The unit to add. + /// A resource builder. + public static ResourceBuilder WithUnit(this ResourceBuilder resourceBuilder, string unit) + { + return resourceBuilder.WithProperty(UnitKey, unit); + } - /// - /// Adds a description. - /// - /// The resource builder. - /// The description to add. - /// A resource builder. - public static ResourceBuilder WithDescription(this ResourceBuilder resourceBuilder, string description) - { - return resourceBuilder.WithProperty(DescriptionKey, description); - } + /// + /// Adds a description. + /// + /// The resource builder. + /// The description to add. + /// A resource builder. + public static ResourceBuilder WithDescription(this ResourceBuilder resourceBuilder, string description) + { + return resourceBuilder.WithProperty(DescriptionKey, description); + } - /// - /// Adds a warning. - /// - /// The resource builder. - /// The warning to add. - /// A resource builder. - public static ResourceBuilder WithWarning(this ResourceBuilder resourceBuilder, string warning) - { - return resourceBuilder.WithProperty(WarningKey, warning); - } + /// + /// Adds a warning. + /// + /// The resource builder. + /// The warning to add. + /// A resource builder. + public static ResourceBuilder WithWarning(this ResourceBuilder resourceBuilder, string warning) + { + return resourceBuilder.WithProperty(WarningKey, warning); + } - /// - /// Adds groups. - /// - /// The resource builder. - /// The groups to add. - /// A resource builder. - public static ResourceBuilder WithGroups(this ResourceBuilder resourceBuilder, params string[] groups) - { - return resourceBuilder.WithProperty(GroupsKey, new JsonArray(groups.Select(group => (JsonNode)group!).ToArray())); - } + /// + /// Adds groups. + /// + /// The resource builder. + /// The groups to add. + /// A resource builder. + public static ResourceBuilder WithGroups(this ResourceBuilder resourceBuilder, params string[] groups) + { + return resourceBuilder.WithProperty(GroupsKey, new JsonArray(groups.Select(group => (JsonNode)group!).ToArray())); + } - #endregion + #endregion - #region Misc + #region Misc - /// - /// Converts a url into a local file path. - /// - /// The url to convert. - /// The local file path. - public static string ToPath(this Uri url) - { - var isRelativeUri = !url.IsAbsoluteUri; + /// + /// Converts a url into a local file path. + /// + /// The url to convert. + /// The local file path. + public static string ToPath(this Uri url) + { + var isRelativeUri = !url.IsAbsoluteUri; - if (isRelativeUri) - return url.ToString(); + if (isRelativeUri) + return url.ToString(); - else if (url.IsFile) - return url.LocalPath.Replace('\\', '/'); + else if (url.IsFile) + return url.LocalPath.Replace('\\', '/'); - else - throw new Exception("Only a file URI can be converted to a path."); - } + else + throw new Exception("Only a file URI can be converted to a path."); + } - // keep in sync with Nexus.UI.Utilities ... - private const int NS_PER_TICK = 100; - private static readonly long[] _nanoseconds = new[] { (long)1e0, (long)1e3, (long)1e6, (long)1e9, (long)60e9, (long)3600e9, (long)86400e9 }; - private static readonly int[] _quotients = new[] { 1000, 1000, 1000, 60, 60, 24, 1 }; - private static readonly string[] _postFixes = new[] { "ns", "us", "ms", "s", "min", "h", "d" }; - // ... except this line - private static readonly Regex _unitStringEvaluator = new(@"^([0-9]+)_([a-z]+)$", RegexOptions.Compiled); - - /// - /// Converts period into a human readable number string with unit. - /// - /// The period to convert. - /// The human readable number string with unit. - public static string ToUnitString(this TimeSpan samplePeriod) - { - var currentValue = samplePeriod.Ticks * NS_PER_TICK; + // keep in sync with Nexus.UI.Utilities ... + private const int NS_PER_TICK = 100; + private static readonly long[] _nanoseconds = new[] { (long)1e0, (long)1e3, (long)1e6, (long)1e9, (long)60e9, (long)3600e9, (long)86400e9 }; + private static readonly int[] _quotients = new[] { 1000, 1000, 1000, 60, 60, 24, 1 }; + private static readonly string[] _postFixes = new[] { "ns", "us", "ms", "s", "min", "h", "d" }; + // ... except this line + private static readonly Regex _unitStringEvaluator = new(@"^([0-9]+)_([a-z]+)$", RegexOptions.Compiled); - for (int i = 0; i < _postFixes.Length; i++) - { - var quotient = Math.DivRem(currentValue, _quotients[i], out var remainder); + /// + /// Converts period into a human readable number string with unit. + /// + /// The period to convert. + /// The human readable number string with unit. + public static string ToUnitString(this TimeSpan samplePeriod) + { + var currentValue = samplePeriod.Ticks * NS_PER_TICK; - if (remainder != 0) - return $"{currentValue}_{_postFixes[i]}"; + for (int i = 0; i < _postFixes.Length; i++) + { + var quotient = Math.DivRem(currentValue, _quotients[i], out var remainder); - else - currentValue = quotient; - } + if (remainder != 0) + return $"{currentValue}_{_postFixes[i]}"; - return $"{(int)currentValue}_{_postFixes.Last()}"; + else + currentValue = quotient; } - // this method is placed here because it requires access to _postFixes and _nanoseconds - internal static TimeSpan ToSamplePeriod(string unitString) - { - var match = _unitStringEvaluator.Match(unitString); + return $"{(int)currentValue}_{_postFixes.Last()}"; + } - if (!match.Success) - throw new Exception("The provided unit string is invalid."); + // this method is placed here because it requires access to _postFixes and _nanoseconds + internal static TimeSpan ToSamplePeriod(string unitString) + { + var match = _unitStringEvaluator.Match(unitString); - var unitIndex = Array.IndexOf(_postFixes, match.Groups[2].Value); + if (!match.Success) + throw new Exception("The provided unit string is invalid."); - if (unitIndex == -1) - throw new Exception("The provided unit is invalid."); + var unitIndex = Array.IndexOf(_postFixes, match.Groups[2].Value); - var totalNanoSeconds = long.Parse(match.Groups[1].Value) * _nanoseconds[unitIndex]; + if (unitIndex == -1) + throw new Exception("The provided unit is invalid."); - if (totalNanoSeconds % NS_PER_TICK != 0) - throw new Exception("The sample period must be a multiple of 100 ns."); + var totalNanoSeconds = long.Parse(match.Groups[1].Value) * _nanoseconds[unitIndex]; - var ticks = totalNanoSeconds / NS_PER_TICK; + if (totalNanoSeconds % NS_PER_TICK != 0) + throw new Exception("The sample period must be a multiple of 100 ns."); - return new TimeSpan(ticks); - } + var ticks = totalNanoSeconds / NS_PER_TICK; - #endregion + return new TimeSpan(ticks); } + + #endregion } diff --git a/src/extensibility/dotnet-extensibility/DataModel/DataModelTypes.cs b/src/extensibility/dotnet-extensibility/DataModel/DataModelTypes.cs index b4d7b701..c9c3c3d7 100644 --- a/src/extensibility/dotnet-extensibility/DataModel/DataModelTypes.cs +++ b/src/extensibility/dotnet-extensibility/DataModel/DataModelTypes.cs @@ -1,142 +1,141 @@ -namespace Nexus.DataModel +namespace Nexus.DataModel; + +internal enum RepresentationKind { - internal enum RepresentationKind - { - Original = 0, - Resampled = 10, - Mean = 20, - MeanPolarDeg = 30, - Min = 40, - Max = 50, - Std = 60, - Rms = 70, - MinBitwise = 80, - MaxBitwise = 90, - Sum = 100 - } + Original = 0, + Resampled = 10, + Mean = 20, + MeanPolarDeg = 30, + Min = 40, + Max = 50, + Std = 60, + Rms = 70, + MinBitwise = 80, + MaxBitwise = 90, + Sum = 100 +} +/// +/// Specifies the Nexus data type. +/// +public enum NexusDataType : ushort +{ /// - /// Specifies the Nexus data type. + /// Unsigned 8-bit integer. /// - public enum NexusDataType : ushort - { - /// - /// Unsigned 8-bit integer. - /// - UINT8 = 0x108, - - /// - /// Signed 8-bit integer. - /// - INT8 = 0x208, - - /// - /// Unsigned 16-bit integer. - /// - UINT16 = 0x110, - - /// - /// Signed 16-bit integer. - /// - INT16 = 0x210, - - /// - /// Unsigned 32-bit integer. - /// - UINT32 = 0x120, - - /// - /// Signed 32-bit integer. - /// - INT32 = 0x220, - - /// - /// Unsigned 64-bit integer. - /// - UINT64 = 0x140, - - /// - /// Signed 64-bit integer. - /// - INT64 = 0x240, - - /// - /// 32-bit floating-point number. - /// - FLOAT32 = 0x320, - - /// - /// 64-bit floating-point number. - /// - FLOAT64 = 0x340 - } + UINT8 = 0x108, + + /// + /// Signed 8-bit integer. + /// + INT8 = 0x208, + + /// + /// Unsigned 16-bit integer. + /// + UINT16 = 0x110, + + /// + /// Signed 16-bit integer. + /// + INT16 = 0x210, + + /// + /// Unsigned 32-bit integer. + /// + UINT32 = 0x120, + + /// + /// Signed 32-bit integer. + /// + INT32 = 0x220, + + /// + /// Unsigned 64-bit integer. + /// + UINT64 = 0x140, /// - /// A catalog item consists of a catalog, a resource and a representation. + /// Signed 64-bit integer. /// - /// The catalog. - /// The resource. - /// The representation. - /// The optional dictionary of representation parameters and its arguments. - public record CatalogItem(ResourceCatalog Catalog, Resource Resource, Representation Representation, IReadOnlyDictionary? Parameters) + INT64 = 0x240, + + /// + /// 32-bit floating-point number. + /// + FLOAT32 = 0x320, + + /// + /// 64-bit floating-point number. + /// + FLOAT64 = 0x340 +} + +/// +/// A catalog item consists of a catalog, a resource and a representation. +/// +/// The catalog. +/// The resource. +/// The representation. +/// The optional dictionary of representation parameters and its arguments. +public record CatalogItem(ResourceCatalog Catalog, Resource Resource, Representation Representation, IReadOnlyDictionary? Parameters) +{ + /// + /// Construct a fully qualified path. + /// + /// The fully qualified path. + public string ToPath() { - /// - /// Construct a fully qualified path. - /// - /// The fully qualified path. - public string ToPath() - { - var parametersString = DataModelUtilities.GetRepresentationParameterString(Parameters); - return $"{Catalog.Id}/{Resource.Id}/{Representation.Id}{parametersString}"; - } + var parametersString = DataModelUtilities.GetRepresentationParameterString(Parameters); + return $"{Catalog.Id}/{Resource.Id}/{Representation.Id}{parametersString}"; } +} +/// +/// A catalog registration. +/// +/// The absolute or relative path of the catalog. +/// A nullable title. +/// A boolean which indicates if the catalog and its children should be reloaded on each request. +public record CatalogRegistration(string Path, string? Title, bool IsTransient = false) +{ /// - /// A catalog registration. + /// Gets the absolute or relative path of the catalog. /// - /// The absolute or relative path of the catalog. - /// A nullable title. - /// A boolean which indicates if the catalog and its children should be reloaded on each request. - public record CatalogRegistration(string Path, string? Title, bool IsTransient = false) + public string Path { get; init; } = IsValidPath(Path) + ? Path + : throw new ArgumentException($"The catalog path {Path} is not valid."); + + /// + /// Gets the nullable title. + /// + public string? Title { get; } = Title; + + /// + /// Gets a boolean which indicates if the catalog and its children should be reloaded on each request. + /// + public bool IsTransient { get; } = IsTransient; + + private static bool IsValidPath(string path) { - /// - /// Gets the absolute or relative path of the catalog. - /// - public string Path { get; init; } = IsValidPath(Path) - ? Path - : throw new ArgumentException($"The catalog path {Path} is not valid."); - - /// - /// Gets the nullable title. - /// - public string? Title { get; } = Title; - - /// - /// Gets a boolean which indicates if the catalog and its children should be reloaded on each request. - /// - public bool IsTransient { get; } = IsTransient; - - private static bool IsValidPath(string path) - { - if (path == "/") - return true; - - if (!path.StartsWith("/")) - path = "/" + path; - - var result = ResourceCatalog.ValidIdExpression.IsMatch(path); - - return result; - } - } + if (path == "/") + return true; + + if (!path.StartsWith("/")) + path = "/" + path; - // keep in sync with Nexus.UI.Utilities - internal record ResourcePathParseResult( - string CatalogId, - string ResourceId, - TimeSpan SamplePeriod, - RepresentationKind Kind, - string? Parameters, - TimeSpan? BasePeriod - ); + var result = ResourceCatalog.ValidIdExpression.IsMatch(path); + + return result; + } } + +// keep in sync with Nexus.UI.Utilities +internal record ResourcePathParseResult( + string CatalogId, + string ResourceId, + TimeSpan SamplePeriod, + RepresentationKind Kind, + string? Parameters, + TimeSpan? BasePeriod +); diff --git a/src/extensibility/dotnet-extensibility/DataModel/DataModelUtilities.cs b/src/extensibility/dotnet-extensibility/DataModel/DataModelUtilities.cs index 3270e566..9322846e 100644 --- a/src/extensibility/dotnet-extensibility/DataModel/DataModelUtilities.cs +++ b/src/extensibility/dotnet-extensibility/DataModel/DataModelUtilities.cs @@ -3,287 +3,286 @@ using System.Text.Json.Nodes; using System.Text.RegularExpressions; -namespace Nexus.DataModel +namespace Nexus.DataModel; + +internal static class DataModelUtilities { - internal static class DataModelUtilities + /* Example resource paths: + * + * /a/b/c/T1/10_ms + * /a/b/c/T1/10_ms(abc=456) + * /a/b/c/T1/10_ms(abc=456)#base=1s + * /a/b/c/T1/600_s_mean + * /a/b/c/T1/600_s_mean(abc=456) + * /a/b/c/T1/600_s_mean#base=1s + * /a/b/c/T1/600_s_mean(abc=456)#base=1s + */ + // keep in sync with Nexus.UI.Core.Utilities + private static readonly Regex _resourcePathEvaluator = new(@"^(?'catalog'.*)\/(?'resource'.*)\/(?'sample_period'[0-9]+_[a-zA-Z]+)(?:_(?'kind'[^\(#\s]+))?(?:\((?'parameters'.*)\))?(?:#(?'fragment'.*))?$", RegexOptions.Compiled); + + private static string ToPascalCase(string input) { - /* Example resource paths: - * - * /a/b/c/T1/10_ms - * /a/b/c/T1/10_ms(abc=456) - * /a/b/c/T1/10_ms(abc=456)#base=1s - * /a/b/c/T1/600_s_mean - * /a/b/c/T1/600_s_mean(abc=456) - * /a/b/c/T1/600_s_mean#base=1s - * /a/b/c/T1/600_s_mean(abc=456)#base=1s - */ - // keep in sync with Nexus.UI.Core.Utilities - private static readonly Regex _resourcePathEvaluator = new(@"^(?'catalog'.*)\/(?'resource'.*)\/(?'sample_period'[0-9]+_[a-zA-Z]+)(?:_(?'kind'[^\(#\s]+))?(?:\((?'parameters'.*)\))?(?:#(?'fragment'.*))?$", RegexOptions.Compiled); - - private static string ToPascalCase(string input) - { - var camelCase = Regex.Replace(input, "_.", match => match.Value[1..].ToUpper()); - var pascalCase = string.Concat(camelCase[0].ToString().ToUpper(), camelCase.AsSpan(1)); + var camelCase = Regex.Replace(input, "_.", match => match.Value[1..].ToUpper()); + var pascalCase = string.Concat(camelCase[0].ToString().ToUpper(), camelCase.AsSpan(1)); - return pascalCase; - } + return pascalCase; + } - // keep in sync with Nexus.UI.Utilities - public static bool TryParseResourcePath( - string resourcePath, - [NotNullWhen(returnValue: true)] out ResourcePathParseResult? parseResult) - { - parseResult = default; + // keep in sync with Nexus.UI.Utilities + public static bool TryParseResourcePath( + string resourcePath, + [NotNullWhen(returnValue: true)] out ResourcePathParseResult? parseResult) + { + parseResult = default; - // match - var match = _resourcePathEvaluator.Match(resourcePath); + // match + var match = _resourcePathEvaluator.Match(resourcePath); - if (!match.Success) - return false; + if (!match.Success) + return false; - // kind - var kind = RepresentationKind.Original; + // kind + var kind = RepresentationKind.Original; - if (match.Groups["kind"].Success) - { - var rawValue = match.Groups["kind"].Value; + if (match.Groups["kind"].Success) + { + var rawValue = match.Groups["kind"].Value; - if (!Enum.TryParse(ToPascalCase(rawValue), out kind)) - return default; - } + if (!Enum.TryParse(ToPascalCase(rawValue), out kind)) + return default; + } - // basePeriod - TimeSpan? basePeriod = default; + // basePeriod + TimeSpan? basePeriod = default; - if (match.Groups["fragment"].Success) - { - var unitString = match.Groups["fragment"].Value.Split('=', count: 2)[1]; - basePeriod = DataModelExtensions.ToSamplePeriod(unitString); - } - - // result - parseResult = new ResourcePathParseResult( - CatalogId: match.Groups["catalog"].Value, - ResourceId: match.Groups["resource"].Value, - SamplePeriod: DataModelExtensions.ToSamplePeriod(match.Groups["sample_period"].Value), - Kind: kind, - Parameters: match.Groups["parameters"].Success ? match.Groups["parameters"].Value : default, - BasePeriod: basePeriod - ); - - return true; + if (match.Groups["fragment"].Success) + { + var unitString = match.Groups["fragment"].Value.Split('=', count: 2)[1]; + basePeriod = DataModelExtensions.ToSamplePeriod(unitString); } - public static string? GetRepresentationParameterString(IReadOnlyDictionary? parameters) - { - if (parameters is null) - return default; + // result + parseResult = new ResourcePathParseResult( + CatalogId: match.Groups["catalog"].Value, + ResourceId: match.Groups["resource"].Value, + SamplePeriod: DataModelExtensions.ToSamplePeriod(match.Groups["sample_period"].Value), + Kind: kind, + Parameters: match.Groups["parameters"].Success ? match.Groups["parameters"].Value : default, + BasePeriod: basePeriod + ); + + return true; + } - var serializedParameters = parameters - .Select(parameter => $"{parameter.Key}={parameter.Value}"); + public static string? GetRepresentationParameterString(IReadOnlyDictionary? parameters) + { + if (parameters is null) + return default; - var parametersString = $"({string.Join(',', serializedParameters)})"; + var serializedParameters = parameters + .Select(parameter => $"{parameter.Key}={parameter.Value}"); - return parametersString; - } + var parametersString = $"({string.Join(',', serializedParameters)})"; - public static List? MergeResources(IReadOnlyList? resources1, IReadOnlyList? resources2) - { - if (resources1 is null && resources2 is null) - return null; + return parametersString; + } - if (resources1 is null) - return resources2! - .Select(resource => resource.DeepCopy()) - .ToList(); + public static List? MergeResources(IReadOnlyList? resources1, IReadOnlyList? resources2) + { + if (resources1 is null && resources2 is null) + return null; - if (resources2 is null) - return resources1! - .Select(resource => resource.DeepCopy()) - .ToList(); + if (resources1 is null) + return resources2! + .Select(resource => resource.DeepCopy()) + .ToList(); - var mergedResources = resources1 + if (resources2 is null) + return resources1! .Select(resource => resource.DeepCopy()) .ToList(); - foreach (var newResource in resources2) - { - var index = mergedResources.FindIndex(current => current.Id == newResource.Id); + var mergedResources = resources1 + .Select(resource => resource.DeepCopy()) + .ToList(); - if (index >= 0) - { - mergedResources[index] = mergedResources[index].Merge(newResource); - } + foreach (var newResource in resources2) + { + var index = mergedResources.FindIndex(current => current.Id == newResource.Id); - else - { - mergedResources.Add(newResource.DeepCopy()); - } + if (index >= 0) + { + mergedResources[index] = mergedResources[index].Merge(newResource); } - return mergedResources; + else + { + mergedResources.Add(newResource.DeepCopy()); + } } - public static List? MergeRepresentations(IReadOnlyList? representations1, IReadOnlyList? representations2) - { - if (representations1 is null && representations2 is null) - return null; + return mergedResources; + } - if (representations1 is null) - return representations2! - .Select(representation => representation.DeepCopy()) - .ToList(); + public static List? MergeRepresentations(IReadOnlyList? representations1, IReadOnlyList? representations2) + { + if (representations1 is null && representations2 is null) + return null; - if (representations2 is null) - return representations1! - .Select(representation => representation.DeepCopy()) - .ToList(); + if (representations1 is null) + return representations2! + .Select(representation => representation.DeepCopy()) + .ToList(); - var mergedRepresentations = representations1 + if (representations2 is null) + return representations1! .Select(representation => representation.DeepCopy()) .ToList(); - foreach (var newRepresentation in representations2) - { - var index = mergedRepresentations.FindIndex(current => current.Id == newRepresentation.Id); + var mergedRepresentations = representations1 + .Select(representation => representation.DeepCopy()) + .ToList(); - if (index >= 0) - { - if (!newRepresentation.Equals(mergedRepresentations[index])) - throw new Exception("The representations to be merged are not equal."); + foreach (var newRepresentation in representations2) + { + var index = mergedRepresentations.FindIndex(current => current.Id == newRepresentation.Id); - } + if (index >= 0) + { + if (!newRepresentation.Equals(mergedRepresentations[index])) + throw new Exception("The representations to be merged are not equal."); - else - { - mergedRepresentations.Add(newRepresentation); - } } - return mergedRepresentations; + else + { + mergedRepresentations.Add(newRepresentation); + } } - public static IReadOnlyDictionary? MergeProperties(IReadOnlyDictionary? properties1, IReadOnlyDictionary? properties2) - { - if (properties1 is null) - return properties2; + return mergedRepresentations; + } - if (properties2 is null) - return properties1; + public static IReadOnlyDictionary? MergeProperties(IReadOnlyDictionary? properties1, IReadOnlyDictionary? properties2) + { + if (properties1 is null) + return properties2; - var mergedProperties = properties1.ToDictionary(entry => entry.Key, entry => entry.Value); + if (properties2 is null) + return properties1; - foreach (var entry in properties2) - { - if (mergedProperties.ContainsKey(entry.Key)) - mergedProperties[entry.Key] = MergeProperties(properties1[entry.Key], entry.Value); + var mergedProperties = properties1.ToDictionary(entry => entry.Key, entry => entry.Value); - else - mergedProperties[entry.Key] = entry.Value; - } + foreach (var entry in properties2) + { + if (mergedProperties.ContainsKey(entry.Key)) + mergedProperties[entry.Key] = MergeProperties(properties1[entry.Key], entry.Value); - return mergedProperties; + else + mergedProperties[entry.Key] = entry.Value; } - public static JsonElement MergeProperties(JsonElement properties1, JsonElement properties2) - { - var properties1IsNotOK = properties1.ValueKind == JsonValueKind.Null; - var properties2IsNotOK = properties2.ValueKind == JsonValueKind.Null; + return mergedProperties; + } - if (properties1IsNotOK) - return properties2; + public static JsonElement MergeProperties(JsonElement properties1, JsonElement properties2) + { + var properties1IsNotOK = properties1.ValueKind == JsonValueKind.Null; + var properties2IsNotOK = properties2.ValueKind == JsonValueKind.Null; - if (properties2IsNotOK) - return properties1; + if (properties1IsNotOK) + return properties2; - JsonNode mergedProperties; + if (properties2IsNotOK) + return properties1; - if (properties1.ValueKind == JsonValueKind.Object && properties2.ValueKind == JsonValueKind.Object) - { - mergedProperties = new JsonObject(); - MergeObjects((JsonObject)mergedProperties, properties1, properties2); - } + JsonNode mergedProperties; - else if (properties1.ValueKind == JsonValueKind.Array && properties2.ValueKind == JsonValueKind.Array) - { - mergedProperties = new JsonArray(); - MergeArrays((JsonArray)mergedProperties, properties1, properties2); - } + if (properties1.ValueKind == JsonValueKind.Object && properties2.ValueKind == JsonValueKind.Object) + { + mergedProperties = new JsonObject(); + MergeObjects((JsonObject)mergedProperties, properties1, properties2); + } - else - { - return properties2; - } + else if (properties1.ValueKind == JsonValueKind.Array && properties2.ValueKind == JsonValueKind.Array) + { + mergedProperties = new JsonArray(); + MergeArrays((JsonArray)mergedProperties, properties1, properties2); + } - return JsonSerializer.SerializeToElement(mergedProperties); + else + { + return properties2; } - private static void MergeObjects(JsonObject currentObject, JsonElement root1, JsonElement root2) + return JsonSerializer.SerializeToElement(mergedProperties); + } + + private static void MergeObjects(JsonObject currentObject, JsonElement root1, JsonElement root2) + { + foreach (var property in root1.EnumerateObject()) { - foreach (var property in root1.EnumerateObject()) + if (root2.TryGetProperty(property.Name, out JsonElement newValue) && newValue.ValueKind != JsonValueKind.Null) { - if (root2.TryGetProperty(property.Name, out JsonElement newValue) && newValue.ValueKind != JsonValueKind.Null) - { - var originalValue = property.Value; - var originalValueKind = originalValue.ValueKind; + var originalValue = property.Value; + var originalValueKind = originalValue.ValueKind; - if (newValue.ValueKind == JsonValueKind.Object && originalValueKind == JsonValueKind.Object) - { - var newObject = new JsonObject(); - currentObject[property.Name] = newObject; - - MergeObjects(newObject, originalValue, newValue); - } + if (newValue.ValueKind == JsonValueKind.Object && originalValueKind == JsonValueKind.Object) + { + var newObject = new JsonObject(); + currentObject[property.Name] = newObject; - else if (newValue.ValueKind == JsonValueKind.Array && originalValueKind == JsonValueKind.Array) - { - var newArray = new JsonArray(); - currentObject[property.Name] = newArray; + MergeObjects(newObject, originalValue, newValue); + } - MergeArrays(newArray, originalValue, newValue); - } + else if (newValue.ValueKind == JsonValueKind.Array && originalValueKind == JsonValueKind.Array) + { + var newArray = new JsonArray(); + currentObject[property.Name] = newArray; - else - { - currentObject[property.Name] = ToJsonNode(newValue); - } + MergeArrays(newArray, originalValue, newValue); } else { - currentObject[property.Name] = ToJsonNode(property.Value); + currentObject[property.Name] = ToJsonNode(newValue); } } - foreach (var property in root2.EnumerateObject()) + else { - if (!root1.TryGetProperty(property.Name, out _)) - currentObject[property.Name] = ToJsonNode(property.Value); + currentObject[property.Name] = ToJsonNode(property.Value); } } - private static void MergeArrays(JsonArray currentArray, JsonElement root1, JsonElement root2) + foreach (var property in root2.EnumerateObject()) { - foreach (var element in root1.EnumerateArray()) - { - currentArray.Add(element); - } + if (!root1.TryGetProperty(property.Name, out _)) + currentObject[property.Name] = ToJsonNode(property.Value); + } + } - foreach (var element in root2.EnumerateArray()) - { - currentArray.Add(element); - } + private static void MergeArrays(JsonArray currentArray, JsonElement root1, JsonElement root2) + { + foreach (var element in root1.EnumerateArray()) + { + currentArray.Add(element); } - public static JsonNode? ToJsonNode(JsonElement element) + foreach (var element in root2.EnumerateArray()) { - return element.ValueKind switch - { - JsonValueKind.Null => null, - JsonValueKind.Object => JsonObject.Create(element), - JsonValueKind.Array => JsonArray.Create(element), - _ => JsonValue.Create(element) - }; + currentArray.Add(element); } } + + public static JsonNode? ToJsonNode(JsonElement element) + { + return element.ValueKind switch + { + JsonValueKind.Null => null, + JsonValueKind.Object => JsonObject.Create(element), + JsonValueKind.Array => JsonArray.Create(element), + _ => JsonValue.Create(element) + }; + } } \ No newline at end of file diff --git a/src/extensibility/dotnet-extensibility/DataModel/PropertiesExtensions.cs b/src/extensibility/dotnet-extensibility/DataModel/PropertiesExtensions.cs index 16b0845b..629f542a 100644 --- a/src/extensibility/dotnet-extensibility/DataModel/PropertiesExtensions.cs +++ b/src/extensibility/dotnet-extensibility/DataModel/PropertiesExtensions.cs @@ -1,148 +1,147 @@ using System.Text.Json; -namespace Nexus.DataModel +namespace Nexus.DataModel; + +// TODO: Remove as soon as there is framework level support (may take a while) +/// +/// A static class with extensions for . +/// +public static class PropertiesExtensions { - // TODO: Remove as soon as there is framework level support (may take a while) /// - /// A static class with extensions for . + /// Reads the value of the specified property as string if it exists. /// - public static class PropertiesExtensions + /// The properties. + /// The propery path. + /// + public static string? GetStringValue(this IReadOnlyDictionary properties, string propertyPath) { - /// - /// Reads the value of the specified property as string if it exists. - /// - /// The properties. - /// The propery path. - /// - public static string? GetStringValue(this IReadOnlyDictionary properties, string propertyPath) + var pathSegments = propertyPath.Split('/').AsSpan(); + + if (properties.TryGetValue(pathSegments[0], out var element)) { - var pathSegments = propertyPath.Split('/').AsSpan(); + pathSegments = pathSegments[1..]; - if (properties.TryGetValue(pathSegments[0], out var element)) + if (pathSegments.Length == 0) { - pathSegments = pathSegments[1..]; - - if (pathSegments.Length == 0) - { - if (element.ValueKind == JsonValueKind.String || element.ValueKind == JsonValueKind.Null) - return element.GetString(); - } - - else - { - var newPropertyPath = string.Join('/', pathSegments.ToArray()); - return element.GetStringValue(newPropertyPath); - } + if (element.ValueKind == JsonValueKind.String || element.ValueKind == JsonValueKind.Null) + return element.GetString(); } - return default; + else + { + var newPropertyPath = string.Join('/', pathSegments.ToArray()); + return element.GetStringValue(newPropertyPath); + } } - /// - /// Reads the value of the specified property as string if it exists. - /// - /// The properties. - /// The propery path. - /// - public static string? GetStringValue(this JsonElement properties, string propertyPath) - { - var pathSegments = propertyPath.Split('/').AsSpan(); - var root = properties.GetJsonObjectFromPath(pathSegments[0..^1]); + return default; + } - var propertyName = pathSegments.Length == 0 - ? propertyPath - : pathSegments[^1]; + /// + /// Reads the value of the specified property as string if it exists. + /// + /// The properties. + /// The propery path. + /// + public static string? GetStringValue(this JsonElement properties, string propertyPath) + { + var pathSegments = propertyPath.Split('/').AsSpan(); + var root = properties.GetJsonObjectFromPath(pathSegments[0..^1]); - if (root.ValueKind == JsonValueKind.Object && - root.TryGetProperty(propertyName, out var propertyValue) && - (propertyValue.ValueKind == JsonValueKind.String || propertyValue.ValueKind == JsonValueKind.Null)) - return propertyValue.GetString(); + var propertyName = pathSegments.Length == 0 + ? propertyPath + : pathSegments[^1]; - return default; - } + if (root.ValueKind == JsonValueKind.Object && + root.TryGetProperty(propertyName, out var propertyValue) && + (propertyValue.ValueKind == JsonValueKind.String || propertyValue.ValueKind == JsonValueKind.Null)) + return propertyValue.GetString(); + + return default; + } - /// - /// Reads the value of the specified property as string array if it exists. - /// - /// The properties. - /// The property path. - /// - public static string?[]? GetStringArray(this IReadOnlyDictionary properties, string propertyPath) + /// + /// Reads the value of the specified property as string array if it exists. + /// + /// The properties. + /// The property path. + /// + public static string?[]? GetStringArray(this IReadOnlyDictionary properties, string propertyPath) + { + var pathSegments = propertyPath.Split('/').AsSpan(); + + if (properties.TryGetValue(pathSegments[0], out var element)) { - var pathSegments = propertyPath.Split('/').AsSpan(); + pathSegments = pathSegments[1..]; - if (properties.TryGetValue(pathSegments[0], out var element)) + if (pathSegments.Length == 0) { - pathSegments = pathSegments[1..]; - - if (pathSegments.Length == 0) - { - if (element.ValueKind == JsonValueKind.Array) - return element - .EnumerateArray() - .Where(current => current.ValueKind == JsonValueKind.String || current.ValueKind == JsonValueKind.Null) - .Select(current => current.GetString()) - .ToArray(); - } - - else - { - var newPropertyPath = string.Join('/', pathSegments.ToArray()); - return element.GetStringArray(newPropertyPath); - } + if (element.ValueKind == JsonValueKind.Array) + return element + .EnumerateArray() + .Where(current => current.ValueKind == JsonValueKind.String || current.ValueKind == JsonValueKind.Null) + .Select(current => current.GetString()) + .ToArray(); } - return default; + else + { + var newPropertyPath = string.Join('/', pathSegments.ToArray()); + return element.GetStringArray(newPropertyPath); + } } - /// - /// Reads the value of the specified property as string array if it exists. - /// - /// The properties. - /// The property path. - /// - public static string?[]? GetStringArray(this JsonElement properties, string propertyPath) - { - var pathSegments = propertyPath.Split('/').AsSpan(); - var root = properties.GetJsonObjectFromPath(pathSegments[0..^1]); - - var propertyName = pathSegments.Length == 0 - ? propertyPath - : pathSegments[^1]; - - if (root.ValueKind == JsonValueKind.Object && - root.TryGetProperty(propertyName, out var propertyValue) && - propertyValue.ValueKind == JsonValueKind.Array) - return propertyValue - .EnumerateArray() - .Where(current => current.ValueKind == JsonValueKind.String || current.ValueKind == JsonValueKind.Null) - .Select(current => current.GetString()) - .ToArray(); - - return default; - } + return default; + } - private static JsonElement GetJsonObjectFromPath(this JsonElement root, Span pathSegements) - { - if (pathSegements.Length == 0) - return root; + /// + /// Reads the value of the specified property as string array if it exists. + /// + /// The properties. + /// The property path. + /// + public static string?[]? GetStringArray(this JsonElement properties, string propertyPath) + { + var pathSegments = propertyPath.Split('/').AsSpan(); + var root = properties.GetJsonObjectFromPath(pathSegments[0..^1]); + + var propertyName = pathSegments.Length == 0 + ? propertyPath + : pathSegments[^1]; + + if (root.ValueKind == JsonValueKind.Object && + root.TryGetProperty(propertyName, out var propertyValue) && + propertyValue.ValueKind == JsonValueKind.Array) + return propertyValue + .EnumerateArray() + .Where(current => current.ValueKind == JsonValueKind.String || current.ValueKind == JsonValueKind.Null) + .Select(current => current.GetString()) + .ToArray(); + + return default; + } - var current = root; + private static JsonElement GetJsonObjectFromPath(this JsonElement root, Span pathSegements) + { + if (pathSegements.Length == 0) + return root; - foreach (var pathSegement in pathSegements) + var current = root; + + foreach (var pathSegement in pathSegements) + { + if (current.ValueKind == JsonValueKind.Object && + current.TryGetProperty(pathSegement, out current)) { - if (current.ValueKind == JsonValueKind.Object && - current.TryGetProperty(pathSegement, out current)) - { - // do nothing - } - else - { - return default; - } + // do nothing + } + else + { + return default; } - - return current; } + + return current; } } \ No newline at end of file diff --git a/src/extensibility/dotnet-extensibility/DataModel/Representation.cs b/src/extensibility/dotnet-extensibility/DataModel/Representation.cs index beb5c586..b2c6a2be 100644 --- a/src/extensibility/dotnet-extensibility/DataModel/Representation.cs +++ b/src/extensibility/dotnet-extensibility/DataModel/Representation.cs @@ -3,154 +3,153 @@ using System.Text.Json.Serialization; using System.Text.RegularExpressions; -namespace Nexus.DataModel +namespace Nexus.DataModel; + +/// +/// A representation is part of a resource. +/// +[DebuggerDisplay("{Id,nq}")] +public record Representation { - /// - /// A representation is part of a resource. - /// - [DebuggerDisplay("{Id,nq}")] - public record Representation - { - #region Fields + #region Fields - private static readonly Regex _snakeCaseEvaluator = new("(?<=[a-z])([A-Z])", RegexOptions.Compiled); - private static readonly HashSet _nexusDataTypeValues = new(Enum.GetValues()); + private static readonly Regex _snakeCaseEvaluator = new("(?<=[a-z])([A-Z])", RegexOptions.Compiled); + private static readonly HashSet _nexusDataTypeValues = new(Enum.GetValues()); - private IReadOnlyDictionary? _parameters; + private IReadOnlyDictionary? _parameters; - #endregion + #endregion - #region Constructors + #region Constructors - /// - /// Initializes a new instance of the . - /// - /// The . - /// The sample period. - /// An optional list of representation parameters. - /// Thrown when the resource identifier, the sample period or the detail values are not valid. - public Representation( - NexusDataType dataType, - TimeSpan samplePeriod, - IReadOnlyDictionary? parameters = default) - : this(dataType, samplePeriod, parameters, RepresentationKind.Original) - { - // - } + /// + /// Initializes a new instance of the . + /// + /// The . + /// The sample period. + /// An optional list of representation parameters. + /// Thrown when the resource identifier, the sample period or the detail values are not valid. + public Representation( + NexusDataType dataType, + TimeSpan samplePeriod, + IReadOnlyDictionary? parameters = default) + : this(dataType, samplePeriod, parameters, RepresentationKind.Original) + { + // + } - internal Representation( - NexusDataType dataType, - TimeSpan samplePeriod, - IReadOnlyDictionary? parameters, - RepresentationKind kind) - { - // data type - if (!_nexusDataTypeValues.Contains(dataType)) - throw new ArgumentException($"The data type {dataType} is not valid."); + internal Representation( + NexusDataType dataType, + TimeSpan samplePeriod, + IReadOnlyDictionary? parameters, + RepresentationKind kind) + { + // data type + if (!_nexusDataTypeValues.Contains(dataType)) + throw new ArgumentException($"The data type {dataType} is not valid."); - DataType = dataType; + DataType = dataType; - // sample period - if (samplePeriod.Equals(default)) - throw new ArgumentException($"The sample period {samplePeriod} is not valid."); + // sample period + if (samplePeriod.Equals(default)) + throw new ArgumentException($"The sample period {samplePeriod} is not valid."); - SamplePeriod = samplePeriod; + SamplePeriod = samplePeriod; - // parameters - Parameters = parameters; + // parameters + Parameters = parameters; - // kind - if (!Enum.IsDefined(typeof(RepresentationKind), kind)) - throw new ArgumentException($"The representation kind {kind} is not valid."); + // kind + if (!Enum.IsDefined(typeof(RepresentationKind), kind)) + throw new ArgumentException($"The representation kind {kind} is not valid."); - Kind = kind; + Kind = kind; - // id - Id = SamplePeriod.ToUnitString(); + // id + Id = SamplePeriod.ToUnitString(); - if (kind != RepresentationKind.Original) - { - var snakeCaseKind = _snakeCaseEvaluator.Replace(kind.ToString(), "_$1").Trim().ToLower(); - Id = $"{Id}_{snakeCaseKind}"; - } + if (kind != RepresentationKind.Original) + { + var snakeCaseKind = _snakeCaseEvaluator.Replace(kind.ToString(), "_$1").Trim().ToLower(); + Id = $"{Id}_{snakeCaseKind}"; } + } - #endregion + #endregion - #region Properties + #region Properties - /// - /// The identifer of the representation. It is constructed using the sample period. - /// - [JsonIgnore] - public string Id { get; } + /// + /// The identifer of the representation. It is constructed using the sample period. + /// + [JsonIgnore] + public string Id { get; } - /// - /// The data type. - /// - public NexusDataType DataType { get; } + /// + /// The data type. + /// + public NexusDataType DataType { get; } - /// - /// The sample period. - /// - public TimeSpan SamplePeriod { get; } + /// + /// The sample period. + /// + public TimeSpan SamplePeriod { get; } - /// - /// The optional list of parameters. - /// - public IReadOnlyDictionary? Parameters + /// + /// The optional list of parameters. + /// + public IReadOnlyDictionary? Parameters + { + get + { + return _parameters; + } + + init { - get - { - return _parameters; - } - - init - { - if (value is not null) - ValidateParameters(value); - - _parameters = value; - } + if (value is not null) + ValidateParameters(value); + + _parameters = value; } + } - /// - /// The representation kind. - /// - [JsonIgnore] + /// + /// The representation kind. + /// + [JsonIgnore] #warning Remove this when https://github.com/RicoSuter/NSwag/issues/4681 is solved - internal RepresentationKind Kind { get; } + internal RepresentationKind Kind { get; } - /// - /// The number of bits per element. - /// - [JsonIgnore] - public int ElementSize => ((int)DataType & 0xFF) >> 3; + /// + /// The number of bits per element. + /// + [JsonIgnore] + public int ElementSize => ((int)DataType & 0xFF) >> 3; - #endregion + #endregion - #region "Methods" + #region "Methods" - internal Representation DeepCopy() - { - return new Representation( - dataType: DataType, - samplePeriod: SamplePeriod, - parameters: Parameters?.ToDictionary(parameter => parameter.Key, parameter => parameter.Value.Clone()), - kind: Kind - ); - } + internal Representation DeepCopy() + { + return new Representation( + dataType: DataType, + samplePeriod: SamplePeriod, + parameters: Parameters?.ToDictionary(parameter => parameter.Key, parameter => parameter.Value.Clone()), + kind: Kind + ); + } - private static void ValidateParameters(IReadOnlyDictionary parameters) + private static void ValidateParameters(IReadOnlyDictionary parameters) + { + foreach (var (key, value) in parameters) { - foreach (var (key, value) in parameters) - { - // resources and arguments have the same requirements regarding their IDs - if (!Resource.ValidIdExpression.IsMatch(key)) - throw new Exception("The representation argument identifier is not valid."); - } + // resources and arguments have the same requirements regarding their IDs + if (!Resource.ValidIdExpression.IsMatch(key)) + throw new Exception("The representation argument identifier is not valid."); } - - #endregion } + + #endregion } \ No newline at end of file diff --git a/src/extensibility/dotnet-extensibility/DataModel/Resource.cs b/src/extensibility/dotnet-extensibility/DataModel/Resource.cs index 35242a51..48f31182 100644 --- a/src/extensibility/dotnet-extensibility/DataModel/Resource.cs +++ b/src/extensibility/dotnet-extensibility/DataModel/Resource.cs @@ -3,147 +3,146 @@ using System.Text.Json.Serialization; using System.Text.RegularExpressions; -namespace Nexus.DataModel +namespace Nexus.DataModel; + +/// +/// A resource is part of a resource catalog and holds a list of representations. +/// +[DebuggerDisplay("{Id,nq}")] +public record Resource { + #region Fields + + private string _id = default!; + private IReadOnlyList? _representations; + + #endregion + + #region Constructors + /// - /// A resource is part of a resource catalog and holds a list of representations. + /// Initializes a new instance of the . /// - [DebuggerDisplay("{Id,nq}")] - public record Resource + /// The resource identifier. + /// The properties. + /// The list of representations. + /// Thrown when the resource identifier is not valid. + public Resource( + string id, + IReadOnlyDictionary? properties = default, + IReadOnlyList? representations = default) { - #region Fields - - private string _id = default!; - private IReadOnlyList? _representations; - - #endregion - - #region Constructors - - /// - /// Initializes a new instance of the . - /// - /// The resource identifier. - /// The properties. - /// The list of representations. - /// Thrown when the resource identifier is not valid. - public Resource( - string id, - IReadOnlyDictionary? properties = default, - IReadOnlyList? representations = default) - { - Id = id; - Properties = properties; - Representations = representations; - } + Id = id; + Properties = properties; + Representations = representations; + } - #endregion + #endregion - #region Properties + #region Properties - /// - /// Gets a regular expression to validate a resource identifier. - /// - [JsonIgnore] + /// + /// Gets a regular expression to validate a resource identifier. + /// + [JsonIgnore] #warning Remove this when https://github.com/RicoSuter/NSwag/issues/4681 is solved - public static Regex ValidIdExpression { get; } = new Regex(@"^[a-zA-Z_][a-zA-Z_0-9]*$"); + public static Regex ValidIdExpression { get; } = new Regex(@"^[a-zA-Z_][a-zA-Z_0-9]*$"); - /// - /// Gets a regular expression to find invalid characters in a resource identifier. - /// - [JsonIgnore] + /// + /// Gets a regular expression to find invalid characters in a resource identifier. + /// + [JsonIgnore] #warning Remove this when https://github.com/RicoSuter/NSwag/issues/4681 is solved - public static Regex InvalidIdCharsExpression { get; } = new Regex(@"[^a-zA-Z_0-9]", RegexOptions.Compiled); + public static Regex InvalidIdCharsExpression { get; } = new Regex(@"[^a-zA-Z_0-9]", RegexOptions.Compiled); - /// - /// Gets a regular expression to find invalid start characters in a resource identifier. - /// - [JsonIgnore] + /// + /// Gets a regular expression to find invalid start characters in a resource identifier. + /// + [JsonIgnore] #warning Remove this when https://github.com/RicoSuter/NSwag/issues/4681 is solved - public static Regex InvalidIdStartCharsExpression { get; } = new Regex(@"^[^a-zA-Z_]+", RegexOptions.Compiled); + public static Regex InvalidIdStartCharsExpression { get; } = new Regex(@"^[^a-zA-Z_]+", RegexOptions.Compiled); - /// - /// Gets the identifier. - /// - public string Id + /// + /// Gets the identifier. + /// + public string Id + { + get { - get - { - return _id; - } - - init - { - if (!ValidIdExpression.IsMatch(value)) - throw new ArgumentException($"The resource identifier {value} is not valid."); - - _id = value; - } + return _id; } - /// - /// Gets the properties. - /// - public IReadOnlyDictionary? Properties { get; init; } - - /// - /// Gets the list of representations. - /// - public IReadOnlyList? Representations + init { - get - { - return _representations; - } - - init - { - if (value is not null) - ValidateRepresentations(value); - - _representations = value; - } + if (!ValidIdExpression.IsMatch(value)) + throw new ArgumentException($"The resource identifier {value} is not valid."); + + _id = value; } + } - #endregion + /// + /// Gets the properties. + /// + public IReadOnlyDictionary? Properties { get; init; } - #region "Methods" + /// + /// Gets the list of representations. + /// + public IReadOnlyList? Representations + { + get + { + return _representations; + } - internal Resource Merge(Resource resource) + init { - if (Id != resource.Id) - throw new ArgumentException("The resources to be merged have different identifiers."); + if (value is not null) + ValidateRepresentations(value); - var mergedProperties = DataModelUtilities.MergeProperties(Properties, resource.Properties); - var mergedRepresentations = DataModelUtilities.MergeRepresentations(Representations, resource.Representations); + _representations = value; + } + } - var merged = resource with - { - Properties = mergedProperties, - Representations = mergedRepresentations - }; + #endregion - return merged; - } + #region "Methods" - internal Resource DeepCopy() - { - return new Resource( - id: Id, - representations: Representations?.Select(representation => representation.DeepCopy()).ToList(), - properties: Properties?.ToDictionary(entry => entry.Key, entry => entry.Value.Clone())); - } + internal Resource Merge(Resource resource) + { + if (Id != resource.Id) + throw new ArgumentException("The resources to be merged have different identifiers."); + + var mergedProperties = DataModelUtilities.MergeProperties(Properties, resource.Properties); + var mergedRepresentations = DataModelUtilities.MergeRepresentations(Representations, resource.Representations); - private static void ValidateRepresentations(IReadOnlyList representations) + var merged = resource with { - var uniqueIds = representations - .Select(current => current.Id) - .Distinct(); + Properties = mergedProperties, + Representations = mergedRepresentations + }; - if (uniqueIds.Count() != representations.Count) - throw new ArgumentException("There are multiple representations with the same identifier."); - } + return merged; + } + + internal Resource DeepCopy() + { + return new Resource( + id: Id, + representations: Representations?.Select(representation => representation.DeepCopy()).ToList(), + properties: Properties?.ToDictionary(entry => entry.Key, entry => entry.Value.Clone())); + } - #endregion + private static void ValidateRepresentations(IReadOnlyList representations) + { + var uniqueIds = representations + .Select(current => current.Id) + .Distinct(); + + if (uniqueIds.Count() != representations.Count) + throw new ArgumentException("There are multiple representations with the same identifier."); } + + #endregion } \ No newline at end of file diff --git a/src/extensibility/dotnet-extensibility/DataModel/ResourceBuilder.cs b/src/extensibility/dotnet-extensibility/DataModel/ResourceBuilder.cs index 07e057c5..c67027cc 100644 --- a/src/extensibility/dotnet-extensibility/DataModel/ResourceBuilder.cs +++ b/src/extensibility/dotnet-extensibility/DataModel/ResourceBuilder.cs @@ -1,99 +1,98 @@ using System.Diagnostics; using System.Text.Json; -namespace Nexus.DataModel +namespace Nexus.DataModel; + +/// +/// A resource builder simplifies building a resource. +/// +[DebuggerDisplay("{Id,nq}")] +public record ResourceBuilder { + #region Fields + + private readonly string _id; + private Dictionary? _properties; + private List? _representations; + + #endregion + + #region Constructors + + /// + /// Initializes a new instance of the . + /// + /// The identifier of the resource to be built. + public ResourceBuilder(string id) + { + _id = id; + } + + #endregion + + #region "Methods" + /// - /// A resource builder simplifies building a resource. + /// Adds a property. /// - [DebuggerDisplay("{Id,nq}")] - public record ResourceBuilder + /// The key of the property. + /// The value of the property. + /// The resource builder. + public ResourceBuilder WithProperty(string key, object value) { - #region Fields - - private readonly string _id; - private Dictionary? _properties; - private List? _representations; - - #endregion - - #region Constructors - - /// - /// Initializes a new instance of the . - /// - /// The identifier of the resource to be built. - public ResourceBuilder(string id) - { - _id = id; - } - - #endregion - - #region "Methods" - - /// - /// Adds a property. - /// - /// The key of the property. - /// The value of the property. - /// The resource builder. - public ResourceBuilder WithProperty(string key, object value) - { - _properties ??= new(); - - _properties[key] = JsonSerializer.SerializeToElement(value); - - return this; - } - - /// - /// Adds a . - /// - /// The . - /// The resource builder. - public ResourceBuilder AddRepresentation(Representation representation) - { - _representations ??= new List(); - - _representations.Add(representation); - - return this; - } - - /// - /// Adds a list of . - /// - /// The list of . - /// The resource builder. - public ResourceBuilder AddRepresentations(params Representation[] representations) - { - return AddRepresentations((IEnumerable)representations); - } - - /// - /// Adds a list of . - /// - /// The list of . - /// The resource builder. - public ResourceBuilder AddRepresentations(IEnumerable representations) - { - _representations ??= new List(); - - _representations.AddRange(representations); - - return this; - } - - /// - /// Builds the . - /// - /// The . - public Resource Build() - { - return new Resource(_id, _properties, _representations); - } - - #endregion + _properties ??= new(); + + _properties[key] = JsonSerializer.SerializeToElement(value); + + return this; } + + /// + /// Adds a . + /// + /// The . + /// The resource builder. + public ResourceBuilder AddRepresentation(Representation representation) + { + _representations ??= new List(); + + _representations.Add(representation); + + return this; + } + + /// + /// Adds a list of . + /// + /// The list of . + /// The resource builder. + public ResourceBuilder AddRepresentations(params Representation[] representations) + { + return AddRepresentations((IEnumerable)representations); + } + + /// + /// Adds a list of . + /// + /// The list of . + /// The resource builder. + public ResourceBuilder AddRepresentations(IEnumerable representations) + { + _representations ??= new List(); + + _representations.AddRange(representations); + + return this; + } + + /// + /// Builds the . + /// + /// The . + public Resource Build() + { + return new Resource(_id, _properties, _representations); + } + + #endregion } \ No newline at end of file diff --git a/src/extensibility/dotnet-extensibility/DataModel/ResourceCatalog.cs b/src/extensibility/dotnet-extensibility/DataModel/ResourceCatalog.cs index 682b7f2a..351981ff 100644 --- a/src/extensibility/dotnet-extensibility/DataModel/ResourceCatalog.cs +++ b/src/extensibility/dotnet-extensibility/DataModel/ResourceCatalog.cs @@ -5,218 +5,217 @@ using System.Text.Json.Serialization; using System.Text.RegularExpressions; -namespace Nexus.DataModel +namespace Nexus.DataModel; + +/// +/// A catalog is a top level element and holds a list of resources. +/// +[DebuggerDisplay("{Id,nq}")] +public record ResourceCatalog { + #region Fields + + private string _id = default!; + private IReadOnlyList? _resources; + + #endregion + + #region Constructors + /// - /// A catalog is a top level element and holds a list of resources. + /// Initializes a new instance of the . /// - [DebuggerDisplay("{Id,nq}")] - public record ResourceCatalog + /// The catalog identifier. + /// The properties. + /// The list of resources. + /// Thrown when the resource identifier is not valid. + public ResourceCatalog( + string id, + IReadOnlyDictionary? properties = default, + IReadOnlyList? resources = default) { - #region Fields - - private string _id = default!; - private IReadOnlyList? _resources; - - #endregion - - #region Constructors - - /// - /// Initializes a new instance of the . - /// - /// The catalog identifier. - /// The properties. - /// The list of resources. - /// Thrown when the resource identifier is not valid. - public ResourceCatalog( - string id, - IReadOnlyDictionary? properties = default, - IReadOnlyList? resources = default) - { - Id = id; - Properties = properties; - Resources = resources; - } + Id = id; + Properties = properties; + Resources = resources; + } - #endregion + #endregion - #region Properties + #region Properties - /// - /// Gets a regular expression to validate a resource catalog identifier. - /// - [JsonIgnore] + /// + /// Gets a regular expression to validate a resource catalog identifier. + /// + [JsonIgnore] #warning Remove this when https://github.com/RicoSuter/NSwag/issues/4681 is solved - public static Regex ValidIdExpression { get; } = new Regex(@"^(?:\/[a-zA-Z_][a-zA-Z_0-9]*)+$", RegexOptions.Compiled); + public static Regex ValidIdExpression { get; } = new Regex(@"^(?:\/[a-zA-Z_][a-zA-Z_0-9]*)+$", RegexOptions.Compiled); - [JsonIgnore] + [JsonIgnore] #warning Remove this when https://github.com/RicoSuter/NSwag/issues/4681 is solved - private static Regex _matchSingleParametersExpression { get; } = new Regex(@"\s*(.+?)\s*=\s*([^,\)]+)\s*,?", RegexOptions.Compiled); + private static Regex _matchSingleParametersExpression { get; } = new Regex(@"\s*(.+?)\s*=\s*([^,\)]+)\s*,?", RegexOptions.Compiled); - /// - /// Gets the identifier. - /// - public string Id + /// + /// Gets the identifier. + /// + public string Id + { + get { - get - { - return _id; - } + return _id; + } - init - { - if (!ValidIdExpression.IsMatch(value)) - throw new ArgumentException($"The resource catalog identifier {value} is not valid."); + init + { + if (!ValidIdExpression.IsMatch(value)) + throw new ArgumentException($"The resource catalog identifier {value} is not valid."); - _id = value; - } + _id = value; } + } - /// - /// Gets the properties. - /// - public IReadOnlyDictionary? Properties { get; init; } + /// + /// Gets the properties. + /// + public IReadOnlyDictionary? Properties { get; init; } - /// - /// Gets the list of representations. - /// - public IReadOnlyList? Resources + /// + /// Gets the list of representations. + /// + public IReadOnlyList? Resources + { + get { - get - { - return _resources; - } + return _resources; + } - init - { - if (value is not null) - ValidateResources(value); + init + { + if (value is not null) + ValidateResources(value); - _resources = value; - } + _resources = value; } + } - #endregion + #endregion - #region "Methods" + #region "Methods" - /// - /// Merges another catalog with this instance. - /// - /// The catalog to merge into this instance. - /// The merged catalog. - public ResourceCatalog Merge(ResourceCatalog catalog) - { - if (Id != catalog.Id) - throw new ArgumentException("The catalogs to be merged have different identifiers."); + /// + /// Merges another catalog with this instance. + /// + /// The catalog to merge into this instance. + /// The merged catalog. + public ResourceCatalog Merge(ResourceCatalog catalog) + { + if (Id != catalog.Id) + throw new ArgumentException("The catalogs to be merged have different identifiers."); - var mergedProperties = DataModelUtilities.MergeProperties(Properties, catalog.Properties); - var mergedResources = DataModelUtilities.MergeResources(Resources, catalog.Resources); + var mergedProperties = DataModelUtilities.MergeProperties(Properties, catalog.Properties); + var mergedResources = DataModelUtilities.MergeResources(Resources, catalog.Resources); - var merged = catalog with - { - Properties = mergedProperties, - Resources = mergedResources - }; + var merged = catalog with + { + Properties = mergedProperties, + Resources = mergedResources + }; - return merged; - } + return merged; + } - internal bool TryFind(ResourcePathParseResult parseResult, [NotNullWhen(true)] out CatalogItem? catalogItem) - { - catalogItem = default; + internal bool TryFind(ResourcePathParseResult parseResult, [NotNullWhen(true)] out CatalogItem? catalogItem) + { + catalogItem = default; - if (parseResult.CatalogId != Id) - return false; + if (parseResult.CatalogId != Id) + return false; - var resource = Resources?.FirstOrDefault(resource => resource.Id == parseResult.ResourceId); + var resource = Resources?.FirstOrDefault(resource => resource.Id == parseResult.ResourceId); - if (resource is null) - return false; + if (resource is null) + return false; - Representation? representation; + Representation? representation; - if (parseResult.Kind == RepresentationKind.Original) - { - var representationId = parseResult.SamplePeriod.ToUnitString(); - representation = resource.Representations?.FirstOrDefault(representation => representation.Id == representationId); - } - else - { - representation = parseResult.BasePeriod is null - ? resource.Representations?.FirstOrDefault() - : resource.Representations?.FirstOrDefault(representation => representation.Id == parseResult.BasePeriod.Value.ToUnitString()); - } + if (parseResult.Kind == RepresentationKind.Original) + { + var representationId = parseResult.SamplePeriod.ToUnitString(); + representation = resource.Representations?.FirstOrDefault(representation => representation.Id == representationId); + } + else + { + representation = parseResult.BasePeriod is null + ? resource.Representations?.FirstOrDefault() + : resource.Representations?.FirstOrDefault(representation => representation.Id == parseResult.BasePeriod.Value.ToUnitString()); + } + + if (representation is null) + return false; - if (representation is null) - return false; + IReadOnlyDictionary? parameters = default; - IReadOnlyDictionary? parameters = default; + if (parseResult.Parameters is not null) + { + var matches = _matchSingleParametersExpression + .Matches(parseResult.Parameters); - if (parseResult.Parameters is not null) + if (matches.Any()) { - var matches = _matchSingleParametersExpression - .Matches(parseResult.Parameters); - - if (matches.Any()) - { - parameters = new ReadOnlyDictionary(matches - .Select(match => (match.Groups[1].Value, match.Groups[2].Value)) - .ToDictionary(tuple => tuple.Item1, tuple => tuple.Item2)); - } + parameters = new ReadOnlyDictionary(matches + .Select(match => (match.Groups[1].Value, match.Groups[2].Value)) + .ToDictionary(tuple => tuple.Item1, tuple => tuple.Item2)); } + } - var parametersAreOK = + var parametersAreOK = - (representation.Parameters is null && parameters is null) || + (representation.Parameters is null && parameters is null) || - (representation.Parameters is not null && parameters is not null && - representation.Parameters.All(current => + (representation.Parameters is not null && parameters is not null && + representation.Parameters.All(current => - parameters.ContainsKey(current.Key) && + parameters.ContainsKey(current.Key) && - (current.Value.GetStringValue("type") == "input-integer" && long.TryParse(parameters[current.Key], out var _) || - current.Value.GetStringValue("type") == "select" && true /* no validation here */))); + (current.Value.GetStringValue("type") == "input-integer" && long.TryParse(parameters[current.Key], out var _) || + current.Value.GetStringValue("type") == "select" && true /* no validation here */))); - if (!parametersAreOK) - return false; + if (!parametersAreOK) + return false; - catalogItem = new CatalogItem( - this with { Resources = default }, - resource with { Representations = default }, - representation, - parameters); + catalogItem = new CatalogItem( + this with { Resources = default }, + resource with { Representations = default }, + representation, + parameters); - return true; - } - - internal CatalogItem Find(string resourcePath) - { - if (!DataModelUtilities.TryParseResourcePath(resourcePath, out var parseResult)) - throw new Exception($"The resource path {resourcePath} is invalid."); + return true; + } - return Find(parseResult); - } + internal CatalogItem Find(string resourcePath) + { + if (!DataModelUtilities.TryParseResourcePath(resourcePath, out var parseResult)) + throw new Exception($"The resource path {resourcePath} is invalid."); - internal CatalogItem Find(ResourcePathParseResult parseResult) - { - if (!TryFind(parseResult, out var catalogItem)) - throw new Exception($"The resource path {parseResult} could not be found."); + return Find(parseResult); + } - return catalogItem; - } + internal CatalogItem Find(ResourcePathParseResult parseResult) + { + if (!TryFind(parseResult, out var catalogItem)) + throw new Exception($"The resource path {parseResult} could not be found."); - private static void ValidateResources(IReadOnlyList resources) - { - var uniqueIds = resources - .Select(current => current.Id) - .Distinct(); + return catalogItem; + } - if (uniqueIds.Count() != resources.Count) - throw new ArgumentException("There are multiple resources with the same identifier."); - } + private static void ValidateResources(IReadOnlyList resources) + { + var uniqueIds = resources + .Select(current => current.Id) + .Distinct(); - #endregion + if (uniqueIds.Count() != resources.Count) + throw new ArgumentException("There are multiple resources with the same identifier."); } + + #endregion } diff --git a/src/extensibility/dotnet-extensibility/DataModel/ResourceCatalogBuilder.cs b/src/extensibility/dotnet-extensibility/DataModel/ResourceCatalogBuilder.cs index 8c36719b..8bb21878 100644 --- a/src/extensibility/dotnet-extensibility/DataModel/ResourceCatalogBuilder.cs +++ b/src/extensibility/dotnet-extensibility/DataModel/ResourceCatalogBuilder.cs @@ -1,112 +1,111 @@ using System.Text.Json; -namespace Nexus.DataModel +namespace Nexus.DataModel; + +/// +/// A resource catalog builder simplifies building a resource catalog. +/// +public record ResourceCatalogBuilder { + #region Fields + + private readonly string _id; + private Dictionary? _properties; + private List? _resources; + + #endregion + + #region Constructors + + /// + /// Initializes a new instance of the . + /// + /// The identifier of the resource catalog to be built. + public ResourceCatalogBuilder(string id) + { + _id = id; + } + + #endregion + + #region "Methods" + + /// + /// Adds a property. + /// + /// The key of the property. + /// The value of the property. + /// The resource catalog builder. + public ResourceCatalogBuilder WithProperty(string key, JsonElement value) + { + _properties ??= new(); + + _properties[key] = value; + + return this; + } + + /// + /// Adds a property. + /// + /// The key of the property. + /// The value of the property. + /// The resource catalog builder. + public ResourceCatalogBuilder WithProperty(string key, object value) + { + _properties ??= new(); + + _properties[key] = JsonSerializer.SerializeToElement(value); + + return this; + } + + /// + /// Adds a . + /// + /// The . + /// The resource catalog builder. + public ResourceCatalogBuilder AddResource(Resource resource) + { + _resources ??= new List(); + + _resources.Add(resource); + + return this; + } + /// - /// A resource catalog builder simplifies building a resource catalog. + /// Adds a list of . /// - public record ResourceCatalogBuilder + /// The list of . + /// The resource catalog builder. + public ResourceCatalogBuilder AddResources(params Resource[] resources) { - #region Fields - - private readonly string _id; - private Dictionary? _properties; - private List? _resources; - - #endregion - - #region Constructors - - /// - /// Initializes a new instance of the . - /// - /// The identifier of the resource catalog to be built. - public ResourceCatalogBuilder(string id) - { - _id = id; - } - - #endregion - - #region "Methods" - - /// - /// Adds a property. - /// - /// The key of the property. - /// The value of the property. - /// The resource catalog builder. - public ResourceCatalogBuilder WithProperty(string key, JsonElement value) - { - _properties ??= new(); - - _properties[key] = value; - - return this; - } - - /// - /// Adds a property. - /// - /// The key of the property. - /// The value of the property. - /// The resource catalog builder. - public ResourceCatalogBuilder WithProperty(string key, object value) - { - _properties ??= new(); - - _properties[key] = JsonSerializer.SerializeToElement(value); - - return this; - } - - /// - /// Adds a . - /// - /// The . - /// The resource catalog builder. - public ResourceCatalogBuilder AddResource(Resource resource) - { - _resources ??= new List(); - - _resources.Add(resource); - - return this; - } - - /// - /// Adds a list of . - /// - /// The list of . - /// The resource catalog builder. - public ResourceCatalogBuilder AddResources(params Resource[] resources) - { - return AddResources((IEnumerable)resources); - } - - /// - /// Adds a list of . - /// - /// The list of . - /// The resource catalog builder. - public ResourceCatalogBuilder AddResources(IEnumerable resources) - { - _resources ??= new List(); - - _resources.AddRange(resources); - - return this; - } - - /// - /// Builds the . - /// - /// The . - public ResourceCatalog Build() - { - return new ResourceCatalog(_id, _properties, _resources); - } - - #endregion + return AddResources((IEnumerable)resources); } + + /// + /// Adds a list of . + /// + /// The list of . + /// The resource catalog builder. + public ResourceCatalogBuilder AddResources(IEnumerable resources) + { + _resources ??= new List(); + + _resources.AddRange(resources); + + return this; + } + + /// + /// Builds the . + /// + /// The . + public ResourceCatalog Build() + { + return new ResourceCatalog(_id, _properties, _resources); + } + + #endregion } diff --git a/src/extensibility/dotnet-extensibility/Extensibility/DataSource/DataSourceTypes.cs b/src/extensibility/dotnet-extensibility/Extensibility/DataSource/DataSourceTypes.cs index 3092db8e..b5feaea7 100644 --- a/src/extensibility/dotnet-extensibility/Extensibility/DataSource/DataSourceTypes.cs +++ b/src/extensibility/dotnet-extensibility/Extensibility/DataSource/DataSourceTypes.cs @@ -2,98 +2,97 @@ using System.Buffers; using System.Text.Json; -namespace Nexus.Extensibility +namespace Nexus.Extensibility; + +/// +/// The starter package for a data source. +/// +/// An optional URL which points to the data. +/// The system configuration. +/// The source configuration. +/// The request configuration. +public record DataSourceContext( + Uri? ResourceLocator, + IReadOnlyDictionary? SystemConfiguration, + IReadOnlyDictionary? SourceConfiguration, + IReadOnlyDictionary? RequestConfiguration); + +/// +/// A read request. +/// +/// The to be read. +/// The data buffer. +/// The status buffer. A value of 0x01 ('1') indicates that the corresponding value in the data buffer is valid, otherwise it is treated as . +public record ReadRequest( + CatalogItem CatalogItem, + Memory Data, + Memory Status); + +/// +/// Reads the requested data. +/// +/// The path to the resource data to stream. +/// Start date/time. +/// End date/time. +/// The buffer to read to the data into. +/// A cancellation token. +/// +public delegate Task ReadDataHandler( + string resourcePath, + DateTime begin, + DateTime end, + Memory buffer, + CancellationToken cancellationToken); + +internal class ReadRequestManager : IDisposable { - /// - /// The starter package for a data source. - /// - /// An optional URL which points to the data. - /// The system configuration. - /// The source configuration. - /// The request configuration. - public record DataSourceContext( - Uri? ResourceLocator, - IReadOnlyDictionary? SystemConfiguration, - IReadOnlyDictionary? SourceConfiguration, - IReadOnlyDictionary? RequestConfiguration); - - /// - /// A read request. - /// - /// The to be read. - /// The data buffer. - /// The status buffer. A value of 0x01 ('1') indicates that the corresponding value in the data buffer is valid, otherwise it is treated as . - public record ReadRequest( - CatalogItem CatalogItem, - Memory Data, - Memory Status); - - /// - /// Reads the requested data. - /// - /// The path to the resource data to stream. - /// Start date/time. - /// End date/time. - /// The buffer to read to the data into. - /// A cancellation token. - /// - public delegate Task ReadDataHandler( - string resourcePath, - DateTime begin, - DateTime end, - Memory buffer, - CancellationToken cancellationToken); - - internal class ReadRequestManager : IDisposable - { - private readonly IMemoryOwner _dataOwner; - private readonly IMemoryOwner _statusOwner; + private readonly IMemoryOwner _dataOwner; + private readonly IMemoryOwner _statusOwner; - public ReadRequestManager(CatalogItem catalogItem, int elementCount) - { - var byteCount = elementCount * catalogItem.Representation.ElementSize; + public ReadRequestManager(CatalogItem catalogItem, int elementCount) + { + var byteCount = elementCount * catalogItem.Representation.ElementSize; - /* data memory */ - var dataOwner = MemoryPool.Shared.Rent(byteCount); - var dataMemory = dataOwner.Memory[..byteCount]; - dataMemory.Span.Clear(); - _dataOwner = dataOwner; + /* data memory */ + var dataOwner = MemoryPool.Shared.Rent(byteCount); + var dataMemory = dataOwner.Memory[..byteCount]; + dataMemory.Span.Clear(); + _dataOwner = dataOwner; - /* status memory */ - var statusOwner = MemoryPool.Shared.Rent(elementCount); - var statusMemory = statusOwner.Memory[..elementCount]; - statusMemory.Span.Clear(); - _statusOwner = statusOwner; + /* status memory */ + var statusOwner = MemoryPool.Shared.Rent(elementCount); + var statusMemory = statusOwner.Memory[..elementCount]; + statusMemory.Span.Clear(); + _statusOwner = statusOwner; - Request = new ReadRequest(catalogItem, dataMemory, statusMemory); - } + Request = new ReadRequest(catalogItem, dataMemory, statusMemory); + } - public ReadRequest Request { get; } + public ReadRequest Request { get; } - #region IDisposable + #region IDisposable - private bool _disposedValue; + private bool _disposedValue; - protected virtual void Dispose(bool disposing) + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) { - if (!_disposedValue) + if (disposing) { - if (disposing) - { - _dataOwner.Dispose(); - _statusOwner.Dispose(); - } - - _disposedValue = true; + _dataOwner.Dispose(); + _statusOwner.Dispose(); } - } - public void Dispose() - { - Dispose(disposing: true); - GC.SuppressFinalize(this); + _disposedValue = true; } + } - #endregion + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); } + + #endregion } diff --git a/src/extensibility/dotnet-extensibility/Extensibility/DataSource/IDataSource.cs b/src/extensibility/dotnet-extensibility/Extensibility/DataSource/IDataSource.cs index 8154fc21..0b714e9c 100644 --- a/src/extensibility/dotnet-extensibility/Extensibility/DataSource/IDataSource.cs +++ b/src/extensibility/dotnet-extensibility/Extensibility/DataSource/IDataSource.cs @@ -1,85 +1,84 @@ using Microsoft.Extensions.Logging; using Nexus.DataModel; -namespace Nexus.Extensibility +namespace Nexus.Extensibility; + +/// +/// A data source. +/// +public interface IDataSource : IExtension { /// - /// A data source. + /// Invoked by Nexus right after construction to provide the context. /// - public interface IDataSource : IExtension - { - /// - /// Invoked by Nexus right after construction to provide the context. - /// - /// The . - /// The logger. - /// A token to cancel the current operation. - /// The task. - Task SetContextAsync( - DataSourceContext context, - ILogger logger, - CancellationToken cancellationToken); + /// The . + /// The logger. + /// A token to cancel the current operation. + /// The task. + Task SetContextAsync( + DataSourceContext context, + ILogger logger, + CancellationToken cancellationToken); - /// - /// Gets the catalog registrations that are located under . - /// - /// The parent path for which to return catalog registrations. - /// A token to cancel the current operation. - /// The catalog identifiers task. - Task GetCatalogRegistrationsAsync( - string path, - CancellationToken cancellationToken); + /// + /// Gets the catalog registrations that are located under . + /// + /// The parent path for which to return catalog registrations. + /// A token to cancel the current operation. + /// The catalog identifiers task. + Task GetCatalogRegistrationsAsync( + string path, + CancellationToken cancellationToken); - /// - /// Gets the requested . - /// - /// The catalog identifier. - /// A token to cancel the current operation. - /// The catalog request task. - Task GetCatalogAsync( - string catalogId, - CancellationToken cancellationToken); + /// + /// Gets the requested . + /// + /// The catalog identifier. + /// A token to cancel the current operation. + /// The catalog request task. + Task GetCatalogAsync( + string catalogId, + CancellationToken cancellationToken); - /// - /// Gets the time range of the . - /// - /// The catalog identifier. - /// A token to cancel the current operation. - /// The time range task. - Task<(DateTime Begin, DateTime End)> GetTimeRangeAsync( - string catalogId, - CancellationToken cancellationToken); + /// + /// Gets the time range of the . + /// + /// The catalog identifier. + /// A token to cancel the current operation. + /// The time range task. + Task<(DateTime Begin, DateTime End)> GetTimeRangeAsync( + string catalogId, + CancellationToken cancellationToken); - /// - /// Gets the availability of the . - /// - /// The catalog identifier. - /// The begin of the availability period. - /// The end of the availability period. - /// A token to cancel the current operation. - /// The availability task. - Task GetAvailabilityAsync( - string catalogId, - DateTime begin, - DateTime end, - CancellationToken cancellationToken); + /// + /// Gets the availability of the . + /// + /// The catalog identifier. + /// The begin of the availability period. + /// The end of the availability period. + /// A token to cancel the current operation. + /// The availability task. + Task GetAvailabilityAsync( + string catalogId, + DateTime begin, + DateTime end, + CancellationToken cancellationToken); - /// - /// Performs a number of read requests. - /// - /// The beginning of the period to read. - /// The end of the period to read. - /// The array of read requests. - /// A delegate to asynchronously read data from Nexus. - /// An object to report the read progress between 0.0 and 1.0. - /// A token to cancel the current operation. - /// The task. - Task ReadAsync( - DateTime begin, - DateTime end, - ReadRequest[] requests, - ReadDataHandler readData, - IProgress progress, - CancellationToken cancellationToken); - } + /// + /// Performs a number of read requests. + /// + /// The beginning of the period to read. + /// The end of the period to read. + /// The array of read requests. + /// A delegate to asynchronously read data from Nexus. + /// An object to report the read progress between 0.0 and 1.0. + /// A token to cancel the current operation. + /// The task. + Task ReadAsync( + DateTime begin, + DateTime end, + ReadRequest[] requests, + ReadDataHandler readData, + IProgress progress, + CancellationToken cancellationToken); } diff --git a/src/extensibility/dotnet-extensibility/Extensibility/DataSource/SimpleDataSource.cs b/src/extensibility/dotnet-extensibility/Extensibility/DataSource/SimpleDataSource.cs index 3e451ad3..bc02f886 100644 --- a/src/extensibility/dotnet-extensibility/Extensibility/DataSource/SimpleDataSource.cs +++ b/src/extensibility/dotnet-extensibility/Extensibility/DataSource/SimpleDataSource.cs @@ -1,78 +1,77 @@ using Microsoft.Extensions.Logging; using Nexus.DataModel; -namespace Nexus.Extensibility +namespace Nexus.Extensibility; + +/// +/// A simple implementation of a data source. +/// +public abstract class SimpleDataSource : IDataSource { + #region Properties + /// - /// A simple implementation of a data source. + /// Gets the data source context. This property is not accessible from within class constructors as it will bet set later. /// - public abstract class SimpleDataSource : IDataSource - { - #region Properties - - /// - /// Gets the data source context. This property is not accessible from within class constructors as it will bet set later. - /// - protected DataSourceContext Context { get; private set; } = default!; + protected DataSourceContext Context { get; private set; } = default!; - /// - /// Gets the data logger. This property is not accessible from within class constructors as it will bet set later. - /// - protected ILogger Logger { get; private set; } = default!; + /// + /// Gets the data logger. This property is not accessible from within class constructors as it will bet set later. + /// + protected ILogger Logger { get; private set; } = default!; - #endregion + #endregion - #region Methods + #region Methods - /// - public Task SetContextAsync( - DataSourceContext context, - ILogger logger, - CancellationToken cancellationToken) - { - Context = context; - Logger = logger; + /// + public Task SetContextAsync( + DataSourceContext context, + ILogger logger, + CancellationToken cancellationToken) + { + Context = context; + Logger = logger; - return Task.CompletedTask; - } + return Task.CompletedTask; + } - /// - public abstract Task GetCatalogRegistrationsAsync( - string path, - CancellationToken cancellationToken); + /// + public abstract Task GetCatalogRegistrationsAsync( + string path, + CancellationToken cancellationToken); - /// - public abstract Task GetCatalogAsync( - string catalogId, - CancellationToken cancellationToken); + /// + public abstract Task GetCatalogAsync( + string catalogId, + CancellationToken cancellationToken); - /// - public virtual Task<(DateTime Begin, DateTime End)> GetTimeRangeAsync( - string catalogId, - CancellationToken cancellationToken) - { - return Task.FromResult((DateTime.MinValue, DateTime.MaxValue)); - } + /// + public virtual Task<(DateTime Begin, DateTime End)> GetTimeRangeAsync( + string catalogId, + CancellationToken cancellationToken) + { + return Task.FromResult((DateTime.MinValue, DateTime.MaxValue)); + } - /// - public virtual Task GetAvailabilityAsync( - string catalogId, - DateTime begin, - DateTime end, - CancellationToken cancellationToken) - { - return Task.FromResult(double.NaN); - } + /// + public virtual Task GetAvailabilityAsync( + string catalogId, + DateTime begin, + DateTime end, + CancellationToken cancellationToken) + { + return Task.FromResult(double.NaN); + } - /// - public abstract Task ReadAsync( - DateTime begin, - DateTime end, - ReadRequest[] requests, - ReadDataHandler readData, - IProgress progress, - CancellationToken cancellationToken); + /// + public abstract Task ReadAsync( + DateTime begin, + DateTime end, + ReadRequest[] requests, + ReadDataHandler readData, + IProgress progress, + CancellationToken cancellationToken); - #endregion - } + #endregion } diff --git a/src/extensibility/dotnet-extensibility/Extensibility/DataWriter/DataWriterTypes.cs b/src/extensibility/dotnet-extensibility/Extensibility/DataWriter/DataWriterTypes.cs index 16467e93..8afc214c 100644 --- a/src/extensibility/dotnet-extensibility/Extensibility/DataWriter/DataWriterTypes.cs +++ b/src/extensibility/dotnet-extensibility/Extensibility/DataWriter/DataWriterTypes.cs @@ -1,43 +1,42 @@ using Nexus.DataModel; using System.Text.Json; -namespace Nexus.Extensibility -{ - /// - /// The starter package for a data writer. - /// - /// The resource locator. - /// The system configuration. - /// The writer configuration. - public record DataWriterContext( - Uri ResourceLocator, - IReadOnlyDictionary? SystemConfiguration, - IReadOnlyDictionary? RequestConfiguration); +namespace Nexus.Extensibility; - /// - /// A write request. - /// - /// The catalog item to be written. - /// The data to be written. - public record WriteRequest( - CatalogItem CatalogItem, - ReadOnlyMemory Data); +/// +/// The starter package for a data writer. +/// +/// The resource locator. +/// The system configuration. +/// The writer configuration. +public record DataWriterContext( + Uri ResourceLocator, + IReadOnlyDictionary? SystemConfiguration, + IReadOnlyDictionary? RequestConfiguration); +/// +/// A write request. +/// +/// The catalog item to be written. +/// The data to be written. +public record WriteRequest( + CatalogItem CatalogItem, + ReadOnlyMemory Data); + +/// +/// An attribute to provide additional information about the data writer. +/// +[AttributeUsage(AttributeTargets.Class)] +public class DataWriterDescriptionAttribute : Attribute +{ /// - /// An attribute to provide additional information about the data writer. + /// Initializes a new instance of the . /// - [AttributeUsage(AttributeTargets.Class)] - public class DataWriterDescriptionAttribute : Attribute + /// The data writer description including the data writer format label and UI options. + public DataWriterDescriptionAttribute(string description) { - /// - /// Initializes a new instance of the . - /// - /// The data writer description including the data writer format label and UI options. - public DataWriterDescriptionAttribute(string description) - { - Description = JsonSerializer.Deserialize?>(description); - } - - internal IReadOnlyDictionary? Description { get; } + Description = JsonSerializer.Deserialize?>(description); } + + internal IReadOnlyDictionary? Description { get; } } diff --git a/src/extensibility/dotnet-extensibility/Extensibility/DataWriter/IDataWriter.cs b/src/extensibility/dotnet-extensibility/Extensibility/DataWriter/IDataWriter.cs index 9f7b5d37..4f913d15 100644 --- a/src/extensibility/dotnet-extensibility/Extensibility/DataWriter/IDataWriter.cs +++ b/src/extensibility/dotnet-extensibility/Extensibility/DataWriter/IDataWriter.cs @@ -1,61 +1,60 @@ using Microsoft.Extensions.Logging; using Nexus.DataModel; -namespace Nexus.Extensibility +namespace Nexus.Extensibility; + +/// +/// A data writer. +/// +public interface IDataWriter : IExtension { /// - /// A data writer. + /// Invoked by Nexus right after construction to provide the context. /// - public interface IDataWriter : IExtension - { - /// - /// Invoked by Nexus right after construction to provide the context. - /// - /// The . - /// The logger. - /// A token to cancel the current operation. - /// The task. - Task SetContextAsync( - DataWriterContext context, - ILogger logger, - CancellationToken cancellationToken); + /// The . + /// The logger. + /// A token to cancel the current operation. + /// The task. + Task SetContextAsync( + DataWriterContext context, + ILogger logger, + CancellationToken cancellationToken); - /// - /// Opens or creates a file for the specified parameters. - /// - /// The beginning of the file. - /// The period of the file. - /// The sample period. - /// An array of catalog items to allow preparation of the file header. - /// A token to cancel the current operation. - /// The task. - Task OpenAsync( - DateTime fileBegin, - TimeSpan filePeriod, - TimeSpan samplePeriod, - CatalogItem[] catalogItems, - CancellationToken cancellationToken); + /// + /// Opens or creates a file for the specified parameters. + /// + /// The beginning of the file. + /// The period of the file. + /// The sample period. + /// An array of catalog items to allow preparation of the file header. + /// A token to cancel the current operation. + /// The task. + Task OpenAsync( + DateTime fileBegin, + TimeSpan filePeriod, + TimeSpan samplePeriod, + CatalogItem[] catalogItems, + CancellationToken cancellationToken); - /// - /// Performs a number of write requests. - /// - /// The offset within the current file. - /// The array of write requests. - /// An object to report the write progress between 0.0 and 1.0. - /// A token to cancel the current operation. - /// The task. - Task WriteAsync( - TimeSpan fileOffset, - WriteRequest[] requests, - IProgress progress, - CancellationToken cancellationToken); + /// + /// Performs a number of write requests. + /// + /// The offset within the current file. + /// The array of write requests. + /// An object to report the write progress between 0.0 and 1.0. + /// A token to cancel the current operation. + /// The task. + Task WriteAsync( + TimeSpan fileOffset, + WriteRequest[] requests, + IProgress progress, + CancellationToken cancellationToken); - /// - /// Closes the current and flushes the data to disk. - /// - /// A token to cancel the current operation. - /// The task. - Task CloseAsync( - CancellationToken cancellationToken); - } + /// + /// Closes the current and flushes the data to disk. + /// + /// A token to cancel the current operation. + /// The task. + Task CloseAsync( + CancellationToken cancellationToken); } \ No newline at end of file diff --git a/src/extensibility/dotnet-extensibility/Extensibility/ExtensibilityUtilities.cs b/src/extensibility/dotnet-extensibility/Extensibility/ExtensibilityUtilities.cs index 3236a980..1246481d 100644 --- a/src/extensibility/dotnet-extensibility/Extensibility/ExtensibilityUtilities.cs +++ b/src/extensibility/dotnet-extensibility/Extensibility/ExtensibilityUtilities.cs @@ -1,43 +1,42 @@ using Nexus.DataModel; using System.Buffers; -namespace Nexus.Extensibility +namespace Nexus.Extensibility; + +/// +/// A static class with useful helper methods. +/// +public static class ExtensibilityUtilities { /// - /// A static class with useful helper methods. + /// Creates buffers of the correct size for a given representation and time period. /// - public static class ExtensibilityUtilities + /// The representation. + /// The beginning of the time period. + /// The end of the time period. + /// The data and status buffers. + public static (Memory, Memory) CreateBuffers(Representation representation, DateTime begin, DateTime end) { - /// - /// Creates buffers of the correct size for a given representation and time period. - /// - /// The representation. - /// The beginning of the time period. - /// The end of the time period. - /// The data and status buffers. - public static (Memory, Memory) CreateBuffers(Representation representation, DateTime begin, DateTime end) - { - var elementCount = CalculateElementCount(begin, end, representation.SamplePeriod); + var elementCount = CalculateElementCount(begin, end, representation.SamplePeriod); - var dataOwner = MemoryPool.Shared.Rent(elementCount * representation.ElementSize); - var data = dataOwner.Memory[..(elementCount * representation.ElementSize)]; - data.Span.Clear(); + var dataOwner = MemoryPool.Shared.Rent(elementCount * representation.ElementSize); + var data = dataOwner.Memory[..(elementCount * representation.ElementSize)]; + data.Span.Clear(); - var statusOwner = MemoryPool.Shared.Rent(elementCount); - var status = statusOwner.Memory[..elementCount]; - status.Span.Clear(); + var statusOwner = MemoryPool.Shared.Rent(elementCount); + var status = statusOwner.Memory[..elementCount]; + status.Span.Clear(); - return (data, status); - } + return (data, status); + } - internal static int CalculateElementCount(DateTime begin, DateTime end, TimeSpan samplePeriod) - { - return (int)((end.Ticks - begin.Ticks) / samplePeriod.Ticks); - } + internal static int CalculateElementCount(DateTime begin, DateTime end, TimeSpan samplePeriod) + { + return (int)((end.Ticks - begin.Ticks) / samplePeriod.Ticks); + } - internal static DateTime RoundDown(DateTime dateTime, TimeSpan timeSpan) - { - return new DateTime(dateTime.Ticks - (dateTime.Ticks % timeSpan.Ticks), dateTime.Kind); - } + internal static DateTime RoundDown(DateTime dateTime, TimeSpan timeSpan) + { + return new DateTime(dateTime.Ticks - (dateTime.Ticks % timeSpan.Ticks), dateTime.Kind); } } \ No newline at end of file diff --git a/src/extensibility/dotnet-extensibility/Extensibility/ExtensionDescriptionAttribute.cs b/src/extensibility/dotnet-extensibility/Extensibility/ExtensionDescriptionAttribute.cs index ace66ef0..b37bf615 100644 --- a/src/extensibility/dotnet-extensibility/Extensibility/ExtensionDescriptionAttribute.cs +++ b/src/extensibility/dotnet-extensibility/Extensibility/ExtensionDescriptionAttribute.cs @@ -1,37 +1,36 @@ -namespace Nexus.Extensibility +namespace Nexus.Extensibility; + +/// +/// An attribute to identify the extension. +/// +[AttributeUsage(validOn: AttributeTargets.Class, AllowMultiple = false)] +public class ExtensionDescriptionAttribute : Attribute { /// - /// An attribute to identify the extension. + /// Initializes a new instance of the . /// - [AttributeUsage(validOn: AttributeTargets.Class, AllowMultiple = false)] - public class ExtensionDescriptionAttribute : Attribute + /// The extension description. + /// An optional project website URL. + /// An optional source repository URL. + public ExtensionDescriptionAttribute(string description, string projectUrl, string repositoryUrl) { - /// - /// Initializes a new instance of the . - /// - /// The extension description. - /// An optional project website URL. - /// An optional source repository URL. - public ExtensionDescriptionAttribute(string description, string projectUrl, string repositoryUrl) - { - Description = description; - ProjectUrl = projectUrl; - RepositoryUrl = repositoryUrl; - } + Description = description; + ProjectUrl = projectUrl; + RepositoryUrl = repositoryUrl; + } - /// - /// Gets the extension description. - /// - public string Description { get; } + /// + /// Gets the extension description. + /// + public string Description { get; } - /// - /// Gets the project website URL. - /// - public string ProjectUrl { get; } + /// + /// Gets the project website URL. + /// + public string ProjectUrl { get; } - /// - /// Gets the source repository URL. - /// - public string RepositoryUrl { get; } - } + /// + /// Gets the source repository URL. + /// + public string RepositoryUrl { get; } } \ No newline at end of file diff --git a/src/extensibility/dotnet-extensibility/Extensibility/IExtension.cs b/src/extensibility/dotnet-extensibility/Extensibility/IExtension.cs index 31fcc769..96c4e9d3 100644 --- a/src/extensibility/dotnet-extensibility/Extensibility/IExtension.cs +++ b/src/extensibility/dotnet-extensibility/Extensibility/IExtension.cs @@ -1,10 +1,9 @@ -namespace Nexus.Extensibility +namespace Nexus.Extensibility; + +/// +/// A base interface for extensions. +/// +public interface IExtension { - /// - /// A base interface for extensions. - /// - public interface IExtension - { - // - } + // } diff --git a/tests/Nexus.Tests/DataSource/DataSourceControllerFixture.cs b/tests/Nexus.Tests/DataSource/DataSourceControllerFixture.cs index d725d7bf..5d47fd37 100644 --- a/tests/Nexus.Tests/DataSource/DataSourceControllerFixture.cs +++ b/tests/Nexus.Tests/DataSource/DataSourceControllerFixture.cs @@ -2,23 +2,22 @@ using Nexus.Extensibility; using Nexus.Sources; -namespace DataSource +namespace DataSource; + +public class DataSourceControllerFixture { - public class DataSourceControllerFixture + public DataSourceControllerFixture() { - public DataSourceControllerFixture() - { - DataSource = new Sample(); + DataSource = new Sample(); - Registration = new InternalDataSourceRegistration( - Id: Guid.NewGuid(), - Type: typeof(Sample).FullName!, - ResourceLocator: default, - Configuration: default); - } + Registration = new InternalDataSourceRegistration( + Id: Guid.NewGuid(), + Type: typeof(Sample).FullName!, + ResourceLocator: default, + Configuration: default); + } - internal IDataSource DataSource { get; } + internal IDataSource DataSource { get; } - internal InternalDataSourceRegistration Registration { get; } - } + internal InternalDataSourceRegistration Registration { get; } } diff --git a/tests/Nexus.Tests/DataSource/DataSourceControllerTests.cs b/tests/Nexus.Tests/DataSource/DataSourceControllerTests.cs index 1f321a49..51d841cf 100644 --- a/tests/Nexus.Tests/DataSource/DataSourceControllerTests.cs +++ b/tests/Nexus.Tests/DataSource/DataSourceControllerTests.cs @@ -10,521 +10,520 @@ using System.Runtime.InteropServices; using Xunit; -namespace DataSource +namespace DataSource; + +public class DataSourceControllerTests : IClassFixture { - public class DataSourceControllerTests : IClassFixture + private readonly DataSourceControllerFixture _fixture; + + public DataSourceControllerTests(DataSourceControllerFixture fixture) { - private readonly DataSourceControllerFixture _fixture; + _fixture = fixture; + } - public DataSourceControllerTests(DataSourceControllerFixture fixture) + [Fact] + internal async Task CanGetAvailability() + { + using var controller = new DataSourceController( + _fixture.DataSource, + _fixture.Registration, + default!, + default!, + default!, + default!, + default!, + NullLogger.Instance); + + await controller.InitializeAsync(default!, default!, CancellationToken.None); + + var catalogId = Sample.LocalCatalogId; + var begin = new DateTime(2020, 01, 01, 00, 00, 00, DateTimeKind.Utc); + var end = new DateTime(2020, 01, 03, 00, 00, 00, DateTimeKind.Utc); + var actual = await controller.GetAvailabilityAsync(catalogId, begin, end, TimeSpan.FromDays(1), CancellationToken.None); + + var expectedData = new double[] { - _fixture = fixture; - } + 1, + 1 + }; - [Fact] - internal async Task CanGetAvailability() - { - using var controller = new DataSourceController( - _fixture.DataSource, - _fixture.Registration, - default!, - default!, - default!, - default!, - default!, - NullLogger.Instance); - - await controller.InitializeAsync(default!, default!, CancellationToken.None); - - var catalogId = Sample.LocalCatalogId; - var begin = new DateTime(2020, 01, 01, 00, 00, 00, DateTimeKind.Utc); - var end = new DateTime(2020, 01, 03, 00, 00, 00, DateTimeKind.Utc); - var actual = await controller.GetAvailabilityAsync(catalogId, begin, end, TimeSpan.FromDays(1), CancellationToken.None); - - var expectedData = new double[] - { - 1, - 1 - }; + Assert.True(expectedData.SequenceEqual(actual.Data)); + } - Assert.True(expectedData.SequenceEqual(actual.Data)); - } + [Fact] + public async Task CanGetTimeRange() + { + using var controller = new DataSourceController( + _fixture.DataSource, + _fixture.Registration, + default!, + default!, + default!, + default!, + default!, + NullLogger.Instance); + + await controller.InitializeAsync(default!, default!, CancellationToken.None); + + var catalogId = Sample.LocalCatalogId; + var actual = await controller.GetTimeRangeAsync(catalogId, CancellationToken.None); + + Assert.Equal(DateTime.MinValue, actual.Begin); + Assert.Equal(DateTime.MaxValue, actual.End); + } - [Fact] - public async Task CanGetTimeRange() - { - using var controller = new DataSourceController( - _fixture.DataSource, - _fixture.Registration, - default!, - default!, - default!, - default!, - default!, - NullLogger.Instance); - - await controller.InitializeAsync(default!, default!, CancellationToken.None); - - var catalogId = Sample.LocalCatalogId; - var actual = await controller.GetTimeRangeAsync(catalogId, CancellationToken.None); - - Assert.Equal(DateTime.MinValue, actual.Begin); - Assert.Equal(DateTime.MaxValue, actual.End); - } - - [Fact] - public async Task CanCheckIsDataOfDayAvailable() + [Fact] + public async Task CanCheckIsDataOfDayAvailable() + { + using var controller = new DataSourceController( + _fixture.DataSource, + _fixture.Registration, + default!, + default!, + default!, + default!, + default!, + NullLogger.Instance); + + await controller.InitializeAsync(default!, default!, CancellationToken.None); + + var day = new DateTime(2020, 01, 01, 0, 0, 0, DateTimeKind.Utc); + var catalogId = Sample.LocalCatalogId; + var actual = await controller.IsDataOfDayAvailableAsync(catalogId, day, CancellationToken.None); + + Assert.True(actual); + } + + [Fact] + public async Task CanRead() + { + using var controller = new DataSourceController( + _fixture.DataSource, + _fixture.Registration, + default!, + default!, + default!, + default!, + default!, + NullLogger.Instance); + + await controller.InitializeAsync(new ConcurrentDictionary(), default!, CancellationToken.None); + + var begin = new DateTime(2020, 01, 01, 0, 0, 0, DateTimeKind.Utc); + var end = new DateTime(2020, 01, 02, 0, 0, 1, DateTimeKind.Utc); + var samplePeriod = TimeSpan.FromSeconds(1); + + // resource 1 + var resourcePath1 = $"{Sample.LocalCatalogId}/V1/1_s"; + var catalogItem1 = (await controller.GetCatalogAsync(Sample.LocalCatalogId, CancellationToken.None)).Find(resourcePath1); + var catalogItemRequest1 = new CatalogItemRequest(catalogItem1, default, default!); + + var pipe1 = new Pipe(); + var dataWriter1 = pipe1.Writer; + + // resource 2 + var resourcePath2 = $"{Sample.LocalCatalogId}/T1/1_s"; + var catalogItem2 = (await controller.GetCatalogAsync(Sample.LocalCatalogId, CancellationToken.None)).Find(resourcePath2); + var catalogItemRequest2 = new CatalogItemRequest(catalogItem2, default, default!); + + var pipe2 = new Pipe(); + var dataWriter2 = pipe2.Writer; + + // combine + var catalogItemRequestPipeWriters = new CatalogItemRequestPipeWriter[] { - using var controller = new DataSourceController( - _fixture.DataSource, - _fixture.Registration, - default!, - default!, - default!, - default!, - default!, - NullLogger.Instance); - - await controller.InitializeAsync(default!, default!, CancellationToken.None); - - var day = new DateTime(2020, 01, 01, 0, 0, 0, DateTimeKind.Utc); - var catalogId = Sample.LocalCatalogId; - var actual = await controller.IsDataOfDayAvailableAsync(catalogId, day, CancellationToken.None); - - Assert.True(actual); - } - - [Fact] - public async Task CanRead() + new CatalogItemRequestPipeWriter(catalogItemRequest1, dataWriter1), + new CatalogItemRequestPipeWriter(catalogItemRequest2, dataWriter2) + }; + + var readingGroups = new DataReadingGroup[] { - using var controller = new DataSourceController( - _fixture.DataSource, - _fixture.Registration, - default!, - default!, - default!, - default!, - default!, - NullLogger.Instance); - - await controller.InitializeAsync(new ConcurrentDictionary(), default!, CancellationToken.None); - - var begin = new DateTime(2020, 01, 01, 0, 0, 0, DateTimeKind.Utc); - var end = new DateTime(2020, 01, 02, 0, 0, 1, DateTimeKind.Utc); - var samplePeriod = TimeSpan.FromSeconds(1); - - // resource 1 - var resourcePath1 = $"{Sample.LocalCatalogId}/V1/1_s"; - var catalogItem1 = (await controller.GetCatalogAsync(Sample.LocalCatalogId, CancellationToken.None)).Find(resourcePath1); - var catalogItemRequest1 = new CatalogItemRequest(catalogItem1, default, default!); - - var pipe1 = new Pipe(); - var dataWriter1 = pipe1.Writer; - - // resource 2 - var resourcePath2 = $"{Sample.LocalCatalogId}/T1/1_s"; - var catalogItem2 = (await controller.GetCatalogAsync(Sample.LocalCatalogId, CancellationToken.None)).Find(resourcePath2); - var catalogItemRequest2 = new CatalogItemRequest(catalogItem2, default, default!); - - var pipe2 = new Pipe(); - var dataWriter2 = pipe2.Writer; - - // combine - var catalogItemRequestPipeWriters = new CatalogItemRequestPipeWriter[] - { - new CatalogItemRequestPipeWriter(catalogItemRequest1, dataWriter1), - new CatalogItemRequestPipeWriter(catalogItemRequest2, dataWriter2) - }; + new DataReadingGroup(controller, catalogItemRequestPipeWriters) + }; - var readingGroups = new DataReadingGroup[] - { - new DataReadingGroup(controller, catalogItemRequestPipeWriters) - }; + double[] result1 = new double[86401]; - double[] result1 = new double[86401]; + var writing1 = Task.Run(async () => + { + Memory resultBuffer1 = result1.AsMemory().Cast(); + var stream1 = pipe1.Reader.AsStream(); - var writing1 = Task.Run(async () => + while (resultBuffer1.Length > 0) { - Memory resultBuffer1 = result1.AsMemory().Cast(); - var stream1 = pipe1.Reader.AsStream(); + // V1 + var readBytes1 = await stream1.ReadAsync(resultBuffer1); - while (resultBuffer1.Length > 0) - { - // V1 - var readBytes1 = await stream1.ReadAsync(resultBuffer1); + if (readBytes1 == 0) + throw new Exception("The stream stopped early."); - if (readBytes1 == 0) - throw new Exception("The stream stopped early."); + resultBuffer1 = resultBuffer1[readBytes1..]; + } + }); - resultBuffer1 = resultBuffer1[readBytes1..]; - } - }); + double[] result2 = new double[86401]; - double[] result2 = new double[86401]; + var writing2 = Task.Run(async () => + { + Memory resultBuffer2 = result2.AsMemory().Cast(); + var stream2 = pipe2.Reader.AsStream(); - var writing2 = Task.Run(async () => + while (resultBuffer2.Length > 0) { - Memory resultBuffer2 = result2.AsMemory().Cast(); - var stream2 = pipe2.Reader.AsStream(); - - while (resultBuffer2.Length > 0) - { - // T1 - var readBytes2 = await stream2.ReadAsync(resultBuffer2); - - if (readBytes2 == 0) - throw new Exception("The stream stopped early."); - - resultBuffer2 = resultBuffer2[readBytes2..]; - } - }); + // T1 + var readBytes2 = await stream2.ReadAsync(resultBuffer2); + + if (readBytes2 == 0) + throw new Exception("The stream stopped early."); + + resultBuffer2 = resultBuffer2[readBytes2..]; + } + }); + + var memoryTracker = Mock.Of(); + + Mock.Get(memoryTracker) + .Setup(memoryTracker => memoryTracker.RegisterAllocationAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync((minium, maximum, _) => new AllocationRegistration(memoryTracker, actualByteCount: maximum)); + + var reading = DataSourceController.ReadAsync( + begin, + end, + samplePeriod, + readingGroups, + default!, + memoryTracker, + progress: default, + NullLogger.Instance, + CancellationToken.None); + + await Task.WhenAll(writing1, writing2, reading); + + // /SAMPLE/LOCAL/V1/1_s + Assert.Equal(6.5, result1[0], precision: 1); + Assert.Equal(6.7, result1[10 * 60 + 1], precision: 1); + Assert.Equal(7.9, result1[01 * 60 * 60 + 2], precision: 1); + Assert.Equal(8.1, result1[02 * 60 * 60 + 3], precision: 1); + Assert.Equal(7.5, result1[10 * 60 * 60 + 4], precision: 1); + + // /SAMPLE/LOCAL/T1/1_s + Assert.Equal(6.5, result2[0], precision: 1); + Assert.Equal(6.7, result2[10 * 60 + 1], precision: 1); + Assert.Equal(7.9, result2[01 * 60 * 60 + 2], precision: 1); + Assert.Equal(8.1, result2[02 * 60 * 60 + 3], precision: 1); + Assert.Equal(7.5, result2[10 * 60 * 60 + 4], precision: 1); + } - var memoryTracker = Mock.Of(); - - Mock.Get(memoryTracker) - .Setup(memoryTracker => memoryTracker.RegisterAllocationAsync(It.IsAny(), It.IsAny(), It.IsAny())) - .ReturnsAsync((minium, maximum, _) => new AllocationRegistration(memoryTracker, actualByteCount: maximum)); - - var reading = DataSourceController.ReadAsync( - begin, - end, - samplePeriod, - readingGroups, - default!, - memoryTracker, - progress: default, - NullLogger.Instance, - CancellationToken.None); - - await Task.WhenAll(writing1, writing2, reading); - - // /SAMPLE/LOCAL/V1/1_s - Assert.Equal(6.5, result1[0], precision: 1); - Assert.Equal(6.7, result1[10 * 60 + 1], precision: 1); - Assert.Equal(7.9, result1[01 * 60 * 60 + 2], precision: 1); - Assert.Equal(8.1, result1[02 * 60 * 60 + 3], precision: 1); - Assert.Equal(7.5, result1[10 * 60 * 60 + 4], precision: 1); - - // /SAMPLE/LOCAL/T1/1_s - Assert.Equal(6.5, result2[0], precision: 1); - Assert.Equal(6.7, result2[10 * 60 + 1], precision: 1); - Assert.Equal(7.9, result2[01 * 60 * 60 + 2], precision: 1); - Assert.Equal(8.1, result2[02 * 60 * 60 + 3], precision: 1); - Assert.Equal(7.5, result2[10 * 60 * 60 + 4], precision: 1); - } - - [Fact] - public async Task CanReadAsStream() + [Fact] + public async Task CanReadAsStream() + { + using var controller = new DataSourceController( + _fixture.DataSource, + _fixture.Registration, + default!, + default!, + default!, + default!, + default!, + NullLogger.Instance); + + await controller.InitializeAsync(new ConcurrentDictionary(), default!, CancellationToken.None); + + var begin = new DateTime(2020, 01, 01, 0, 0, 0, DateTimeKind.Utc); + var end = new DateTime(2020, 01, 02, 0, 0, 1, DateTimeKind.Utc); + var resourcePath = "/SAMPLE/LOCAL/T1/1_s"; + var catalogItem = (await controller.GetCatalogAsync(Sample.LocalCatalogId, CancellationToken.None)).Find(resourcePath); + var catalogItemRequest = new CatalogItemRequest(catalogItem, default, default!); + + var memoryTracker = Mock.Of(); + + Mock.Get(memoryTracker) + .Setup(memoryTracker => memoryTracker.RegisterAllocationAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync((minium, maximum, _) => new AllocationRegistration(memoryTracker, actualByteCount: maximum)); + + var stream = controller.ReadAsStream( + begin, + end, + catalogItemRequest, + default!, + memoryTracker, + NullLogger.Instance, + CancellationToken.None); + + double[] result = new double[86401]; + + await Task.Run(async () => { - using var controller = new DataSourceController( - _fixture.DataSource, - _fixture.Registration, - default!, - default!, - default!, - default!, - default!, - NullLogger.Instance); - - await controller.InitializeAsync(new ConcurrentDictionary(), default!, CancellationToken.None); - - var begin = new DateTime(2020, 01, 01, 0, 0, 0, DateTimeKind.Utc); - var end = new DateTime(2020, 01, 02, 0, 0, 1, DateTimeKind.Utc); - var resourcePath = "/SAMPLE/LOCAL/T1/1_s"; - var catalogItem = (await controller.GetCatalogAsync(Sample.LocalCatalogId, CancellationToken.None)).Find(resourcePath); - var catalogItemRequest = new CatalogItemRequest(catalogItem, default, default!); - - var memoryTracker = Mock.Of(); - - Mock.Get(memoryTracker) - .Setup(memoryTracker => memoryTracker.RegisterAllocationAsync(It.IsAny(), It.IsAny(), It.IsAny())) - .ReturnsAsync((minium, maximum, _) => new AllocationRegistration(memoryTracker, actualByteCount: maximum)); - - var stream = controller.ReadAsStream( - begin, - end, - catalogItemRequest, - default!, - memoryTracker, - NullLogger.Instance, - CancellationToken.None); - - double[] result = new double[86401]; - - await Task.Run(async () => - { - Memory resultBuffer = result.AsMemory().Cast(); + Memory resultBuffer = result.AsMemory().Cast(); - while (resultBuffer.Length > 0) - { - var readBytes = await stream.ReadAsync(resultBuffer); + while (resultBuffer.Length > 0) + { + var readBytes = await stream.ReadAsync(resultBuffer); - if (readBytes == 0) - throw new Exception("This should never happen."); + if (readBytes == 0) + throw new Exception("This should never happen."); - resultBuffer = resultBuffer[readBytes..]; - } - }); + resultBuffer = resultBuffer[readBytes..]; + } + }); - Assert.Equal(86401 * sizeof(double), stream.Length); - Assert.Equal(6.5, result[0], precision: 1); - Assert.Equal(6.7, result[10 * 60 + 1], precision: 1); - Assert.Equal(7.9, result[01 * 60 * 60 + 2], precision: 1); - Assert.Equal(8.1, result[02 * 60 * 60 + 3], precision: 1); - Assert.Equal(7.5, result[10 * 60 * 60 + 4], precision: 1); - } + Assert.Equal(86401 * sizeof(double), stream.Length); + Assert.Equal(6.5, result[0], precision: 1); + Assert.Equal(6.7, result[10 * 60 + 1], precision: 1); + Assert.Equal(7.9, result[01 * 60 * 60 + 2], precision: 1); + Assert.Equal(8.1, result[02 * 60 * 60 + 3], precision: 1); + Assert.Equal(7.5, result[10 * 60 * 60 + 4], precision: 1); + } - [Fact] - public async Task CanReadResampled() - { - // Arrange - var processingService = new Mock(); - - using var controller = new DataSourceController( - _fixture.DataSource, - _fixture.Registration, - default!, - default!, - processingService.Object, - default!, - new DataOptions(), - NullLogger.Instance); - - await controller.InitializeAsync(new ConcurrentDictionary(), default!, CancellationToken.None); - - var begin = new DateTime(2020, 01, 01, 0, 0, 0, 200, DateTimeKind.Utc); - var end = new DateTime(2020, 01, 01, 0, 0, 1, 700, DateTimeKind.Utc); - var pipe = new Pipe(); - var baseItem = (await controller.GetCatalogAsync(Sample.LocalCatalogId, CancellationToken.None)).Find("/SAMPLE/LOCAL/T1/1_s"); - - var item = baseItem with - { - Representation = new Representation( - NexusDataType.FLOAT64, - TimeSpan.FromMilliseconds(100), - parameters: default, - RepresentationKind.Resampled) - }; - - var catalogItemRequest = new CatalogItemRequest(item, baseItem, default!); - - var memoryTracker = Mock.Of(); - - Mock.Get(memoryTracker) - .Setup(memoryTracker => memoryTracker.RegisterAllocationAsync(It.IsAny(), It.IsAny(), It.IsAny())) - .ReturnsAsync(new AllocationRegistration(memoryTracker, actualByteCount: 20000)); - - // Act - await controller.ReadSingleAsync( - begin, - end, - catalogItemRequest, - pipe.Writer, - default!, - memoryTracker, - new Progress(), - NullLogger.Instance, - CancellationToken.None); - - // Assert - processingService - .Verify(processingService => processingService.Resample( - NexusDataType.FLOAT64, - It.IsAny>(), - It.IsAny>(), - It.IsAny>(), - 10, - 2), Times.Exactly(1)); - } - - [Fact] - public async Task CanReadCached() + [Fact] + public async Task CanReadResampled() + { + // Arrange + var processingService = new Mock(); + + using var controller = new DataSourceController( + _fixture.DataSource, + _fixture.Registration, + default!, + default!, + processingService.Object, + default!, + new DataOptions(), + NullLogger.Instance); + + await controller.InitializeAsync(new ConcurrentDictionary(), default!, CancellationToken.None); + + var begin = new DateTime(2020, 01, 01, 0, 0, 0, 200, DateTimeKind.Utc); + var end = new DateTime(2020, 01, 01, 0, 0, 1, 700, DateTimeKind.Utc); + var pipe = new Pipe(); + var baseItem = (await controller.GetCatalogAsync(Sample.LocalCatalogId, CancellationToken.None)).Find("/SAMPLE/LOCAL/T1/1_s"); + + var item = baseItem with { - // Arrange - var expected1 = new double[] { 65, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 101 }; - var expected2 = new double[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25 }; + Representation = new Representation( + NexusDataType.FLOAT64, + TimeSpan.FromMilliseconds(100), + parameters: default, + RepresentationKind.Resampled) + }; + + var catalogItemRequest = new CatalogItemRequest(item, baseItem, default!); + + var memoryTracker = Mock.Of(); + + Mock.Get(memoryTracker) + .Setup(memoryTracker => memoryTracker.RegisterAllocationAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(new AllocationRegistration(memoryTracker, actualByteCount: 20000)); + + // Act + await controller.ReadSingleAsync( + begin, + end, + catalogItemRequest, + pipe.Writer, + default!, + memoryTracker, + new Progress(), + NullLogger.Instance, + CancellationToken.None); + + // Assert + processingService + .Verify(processingService => processingService.Resample( + NexusDataType.FLOAT64, + It.IsAny>(), + It.IsAny>(), + It.IsAny>(), + 10, + 2), Times.Exactly(1)); + } + + [Fact] + public async Task CanReadCached() + { + // Arrange + var expected1 = new double[] { 65, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 101 }; + var expected2 = new double[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25 }; - var begin = new DateTime(2020, 01, 01, 23, 0, 0, DateTimeKind.Utc); - var end = new DateTime(2020, 01, 03, 1, 0, 0, DateTimeKind.Utc); - var samplePeriod = TimeSpan.FromHours(1); + var begin = new DateTime(2020, 01, 01, 23, 0, 0, DateTimeKind.Utc); + var end = new DateTime(2020, 01, 03, 1, 0, 0, DateTimeKind.Utc); + var samplePeriod = TimeSpan.FromHours(1); - var representationBase1 = new Representation(NexusDataType.INT32, TimeSpan.FromMinutes(30), parameters: default, RepresentationKind.Original); - var representation1 = new Representation(NexusDataType.INT32, TimeSpan.FromHours(1), parameters: default, RepresentationKind.Mean); - var representation2 = new Representation(NexusDataType.INT32, TimeSpan.FromHours(1), parameters: default, RepresentationKind.Original); + var representationBase1 = new Representation(NexusDataType.INT32, TimeSpan.FromMinutes(30), parameters: default, RepresentationKind.Original); + var representation1 = new Representation(NexusDataType.INT32, TimeSpan.FromHours(1), parameters: default, RepresentationKind.Mean); + var representation2 = new Representation(NexusDataType.INT32, TimeSpan.FromHours(1), parameters: default, RepresentationKind.Original); - var resource1 = new ResourceBuilder("id1") - .AddRepresentation(representationBase1) - .Build(); + var resource1 = new ResourceBuilder("id1") + .AddRepresentation(representationBase1) + .Build(); - var resource2 = new ResourceBuilder("id2") - .AddRepresentation(representation2) - .Build(); + var resource2 = new ResourceBuilder("id2") + .AddRepresentation(representation2) + .Build(); - var catalog = new ResourceCatalogBuilder("/C1") - .AddResource(resource1) - .AddResource(resource2) - .Build(); + var catalog = new ResourceCatalogBuilder("/C1") + .AddResource(resource1) + .AddResource(resource2) + .Build(); - var baseItem1 = new CatalogItem(catalog, resource1, representationBase1, Parameters: default); - var catalogItem1 = new CatalogItem(catalog, resource1, representation1, Parameters: default); - var catalogItem2 = new CatalogItem(catalog, resource2, representation2, Parameters: default); + var baseItem1 = new CatalogItem(catalog, resource1, representationBase1, Parameters: default); + var catalogItem1 = new CatalogItem(catalog, resource1, representation1, Parameters: default); + var catalogItem2 = new CatalogItem(catalog, resource2, representation2, Parameters: default); - var request1 = new CatalogItemRequest(catalogItem1, baseItem1, default!); - var request2 = new CatalogItemRequest(catalogItem2, default, default!); + var request1 = new CatalogItemRequest(catalogItem1, baseItem1, default!); + var request2 = new CatalogItemRequest(catalogItem2, default, default!); - var pipe1 = new Pipe(); - var pipe2 = new Pipe(); + var pipe1 = new Pipe(); + var pipe2 = new Pipe(); - var catalogItemRequestPipeWriters = new[] - { - new CatalogItemRequestPipeWriter(request1, pipe1.Writer), - new CatalogItemRequestPipeWriter(request2, pipe2.Writer) - }; - - /* IDataSource */ - var dataSource = Mock.Of(); - - Mock.Get(dataSource) - .Setup(dataSource => dataSource.ReadAsync( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny>(), - It.IsAny()) - ) - .Callback, CancellationToken>( - (currentBegin, currentEnd, requests, readDataHandler, progress, cancellationToken) => + var catalogItemRequestPipeWriters = new[] + { + new CatalogItemRequestPipeWriter(request1, pipe1.Writer), + new CatalogItemRequestPipeWriter(request2, pipe2.Writer) + }; + + /* IDataSource */ + var dataSource = Mock.Of(); + + Mock.Get(dataSource) + .Setup(dataSource => dataSource.ReadAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny>(), + It.IsAny()) + ) + .Callback, CancellationToken>( + (currentBegin, currentEnd, requests, readDataHandler, progress, cancellationToken) => + { + var request = requests[0]; + var intData = MemoryMarshal.Cast(request.Data.Span); + + if (request.CatalogItem.Resource.Id == catalogItem1.Resource.Id && + currentBegin == begin) { - var request = requests[0]; - var intData = MemoryMarshal.Cast(request.Data.Span); + Assert.Equal(2, intData.Length); + intData[0] = 33; request.Status.Span[0] = 1; + intData[1] = 97; request.Status.Span[1] = 1; - if (request.CatalogItem.Resource.Id == catalogItem1.Resource.Id && - currentBegin == begin) - { - Assert.Equal(2, intData.Length); - intData[0] = 33; request.Status.Span[0] = 1; - intData[1] = 97; request.Status.Span[1] = 1; + } + else if (request.CatalogItem.Resource.Id == catalogItem1.Resource.Id && + currentBegin == new DateTime(2020, 01, 03, 0, 0, 0, DateTimeKind.Utc)) + { + Assert.Equal(2, intData.Length); + intData[0] = 100; request.Status.Span[0] = 1; + intData[1] = 102; request.Status.Span[1] = 1; + } + else if (request.CatalogItem.Resource.Id == "id2") + { + Assert.Equal(26, intData.Length); - } - else if (request.CatalogItem.Resource.Id == catalogItem1.Resource.Id && - currentBegin == new DateTime(2020, 01, 03, 0, 0, 0, DateTimeKind.Utc)) + for (int i = 0; i < intData.Length; i++) { - Assert.Equal(2, intData.Length); - intData[0] = 100; request.Status.Span[0] = 1; - intData[1] = 102; request.Status.Span[1] = 1; + intData[i] = i; + request.Status.Span[i] = 1; } - else if (request.CatalogItem.Resource.Id == "id2") - { - Assert.Equal(26, intData.Length); + } + else + { + throw new Exception("This should never happen."); + } + }) + .Returns(Task.CompletedTask); + + /* IProcessingService */ + var processingService = Mock.Of(); + + Mock.Get(processingService) + .Setup(processingService => processingService.Aggregate( + It.IsAny(), + It.IsAny(), + It.IsAny>(), + It.IsAny>(), + It.IsAny>(), + It.IsAny())) + .Callback, ReadOnlyMemory, Memory, int>( + (dataType, kind, data, status, targetBuffer, blockSize) => + { + Assert.Equal(NexusDataType.INT32, dataType); + Assert.Equal(RepresentationKind.Mean, kind); + Assert.Equal(8, data.Length); + Assert.Equal(2, status.Length); + Assert.Equal(1, targetBuffer.Length); + Assert.Equal(2, blockSize); + + targetBuffer.Span[0] = (MemoryMarshal.Cast(data.Span)[0] + MemoryMarshal.Cast(data.Span)[1]) / 2.0; + }); - for (int i = 0; i < intData.Length; i++) - { - intData[i] = i; - request.Status.Span[i] = 1; - } - } - else - { - throw new Exception("This should never happen."); - } - }) - .Returns(Task.CompletedTask); - - /* IProcessingService */ - var processingService = Mock.Of(); - - Mock.Get(processingService) - .Setup(processingService => processingService.Aggregate( - It.IsAny(), - It.IsAny(), - It.IsAny>(), - It.IsAny>(), - It.IsAny>(), - It.IsAny())) - .Callback, ReadOnlyMemory, Memory, int>( - (dataType, kind, data, status, targetBuffer, blockSize) => - { - Assert.Equal(NexusDataType.INT32, dataType); - Assert.Equal(RepresentationKind.Mean, kind); - Assert.Equal(8, data.Length); - Assert.Equal(2, status.Length); - Assert.Equal(1, targetBuffer.Length); - Assert.Equal(2, blockSize); - - targetBuffer.Span[0] = (MemoryMarshal.Cast(data.Span)[0] + MemoryMarshal.Cast(data.Span)[1]) / 2.0; - }); - - /* ICacheService */ - var uncachedIntervals = new List + /* ICacheService */ + var uncachedIntervals = new List + { + new Interval(begin, new DateTime(2020, 01, 02, 0, 0, 0, DateTimeKind.Utc)), + new Interval(new DateTime(2020, 01, 03, 0, 0, 0, DateTimeKind.Utc), end) + }; + + var cacheService = new Mock(); + + cacheService + .Setup(cacheService => cacheService.ReadAsync( + It.IsAny(), + It.IsAny(), + It.IsAny>(), + It.IsAny()) + ) + .Callback, CancellationToken>((item, begin, targetBuffer, cancellationToken) => { - new Interval(begin, new DateTime(2020, 01, 02, 0, 0, 0, DateTimeKind.Utc)), - new Interval(new DateTime(2020, 01, 03, 0, 0, 0, DateTimeKind.Utc), end) - }; - - var cacheService = new Mock(); - - cacheService - .Setup(cacheService => cacheService.ReadAsync( - It.IsAny(), - It.IsAny(), - It.IsAny>(), - It.IsAny()) - ) - .Callback, CancellationToken>((item, begin, targetBuffer, cancellationToken) => - { - var offset = 1; - var length = 24; - targetBuffer.Span.Slice(offset, length).Fill(-1); - }) - .Returns(Task.FromResult(uncachedIntervals)); - - /* DataSourceController */ - var registration = new InternalDataSourceRegistration( - Id: Guid.NewGuid(), - "a", - new Uri("http://xyz"), - default, - default); - - var dataSourceController = new DataSourceController( - dataSource, - registration, - default!, - default!, - processingService, - cacheService.Object, - new DataOptions(), - NullLogger.Instance); - - var catalogCache = new ConcurrentDictionary() { [catalog.Id] = catalog }; - - await dataSourceController.InitializeAsync(catalogCache, NullLogger.Instance, CancellationToken.None); - - // Act - await dataSourceController.ReadAsync( - begin, - end, - samplePeriod, - catalogItemRequestPipeWriters, - default!, - new Progress(), - CancellationToken.None); - - // Assert - var actual1 = MemoryMarshal.Cast((await pipe1.Reader.ReadAsync()).Buffer.First.Span).ToArray(); - var actual2 = MemoryMarshal.Cast((await pipe2.Reader.ReadAsync()).Buffer.First.Span).ToArray(); - - Assert.True(expected1.SequenceEqual(actual1)); - Assert.True(expected2.SequenceEqual(actual2)); - - cacheService - .Verify(cacheService => cacheService.UpdateAsync( - catalogItem1, - new DateTime(2020, 01, 01, 23, 0, 0, DateTimeKind.Utc), - It.IsAny>(), - uncachedIntervals, - It.IsAny()), Times.Once()); - } + var offset = 1; + var length = 24; + targetBuffer.Span.Slice(offset, length).Fill(-1); + }) + .Returns(Task.FromResult(uncachedIntervals)); + + /* DataSourceController */ + var registration = new InternalDataSourceRegistration( + Id: Guid.NewGuid(), + "a", + new Uri("http://xyz"), + default, + default); + + var dataSourceController = new DataSourceController( + dataSource, + registration, + default!, + default!, + processingService, + cacheService.Object, + new DataOptions(), + NullLogger.Instance); + + var catalogCache = new ConcurrentDictionary() { [catalog.Id] = catalog }; + + await dataSourceController.InitializeAsync(catalogCache, NullLogger.Instance, CancellationToken.None); + + // Act + await dataSourceController.ReadAsync( + begin, + end, + samplePeriod, + catalogItemRequestPipeWriters, + default!, + new Progress(), + CancellationToken.None); + + // Assert + var actual1 = MemoryMarshal.Cast((await pipe1.Reader.ReadAsync()).Buffer.First.Span).ToArray(); + var actual2 = MemoryMarshal.Cast((await pipe2.Reader.ReadAsync()).Buffer.First.Span).ToArray(); + + Assert.True(expected1.SequenceEqual(actual1)); + Assert.True(expected2.SequenceEqual(actual2)); + + cacheService + .Verify(cacheService => cacheService.UpdateAsync( + catalogItem1, + new DateTime(2020, 01, 01, 23, 0, 0, DateTimeKind.Utc), + It.IsAny>(), + uncachedIntervals, + It.IsAny()), Times.Once()); } } diff --git a/tests/Nexus.Tests/DataSource/SampleDataSourceTests.cs b/tests/Nexus.Tests/DataSource/SampleDataSourceTests.cs index 000c0024..02cd9589 100644 --- a/tests/Nexus.Tests/DataSource/SampleDataSourceTests.cs +++ b/tests/Nexus.Tests/DataSource/SampleDataSourceTests.cs @@ -5,121 +5,120 @@ using Nexus.Sources; using Xunit; -namespace DataSource +namespace DataSource; + +public class SampleDataSourceTests { - public class SampleDataSourceTests + [Fact] + public async Task ProvidesCatalog() + { + // arrange + var dataSource = new Sample() as IDataSource; + + var context = new DataSourceContext( + ResourceLocator: default, + SystemConfiguration: default!, + SourceConfiguration: default!, + RequestConfiguration: default); + + await dataSource.SetContextAsync(context, NullLogger.Instance, CancellationToken.None); + + // act + var actual = await dataSource.GetCatalogAsync(Sample.LocalCatalogId, CancellationToken.None); + + // assert + var actualIds = actual.Resources!.Select(resource => resource.Id).ToList(); + var actualUnits = actual.Resources!.Select(resource => resource.Properties?.GetStringValue("unit")).ToList(); + var actualGroups = actual.Resources!.SelectMany(resource => resource.Properties?.GetStringArray("groups") ?? Array.Empty()); + var actualDataTypes = actual.Resources!.SelectMany(resource => resource.Representations!.Select(representation => representation.DataType)).ToList(); + + var expectedIds = new List() { "T1", "V1", "unix_time1", "unix_time2" }; + var expectedUnits = new List() { "°C", "m/s", default!, default! }; + var expectedGroups = new List() { "Group 1", "Group 1", "Group 2", "Group 2" }; + var expectedDataTypes = new List() { NexusDataType.FLOAT64, NexusDataType.FLOAT64, NexusDataType.FLOAT64, NexusDataType.FLOAT64 }; + + Assert.True(expectedIds.SequenceEqual(actualIds)); + Assert.True(expectedUnits.SequenceEqual(actualUnits)); + Assert.True(expectedGroups.SequenceEqual(actualGroups)); + Assert.True(expectedDataTypes.SequenceEqual(actualDataTypes)); + } + + [Fact] + public async Task CanProvideTimeRange() + { + var dataSource = new Sample() as IDataSource; + + var context = new DataSourceContext( + ResourceLocator: default, + SystemConfiguration: default!, + SourceConfiguration: default!, + RequestConfiguration: default); + + await dataSource.SetContextAsync(context, NullLogger.Instance, CancellationToken.None); + + var (begin, end) = await dataSource.GetTimeRangeAsync("/IN_MEMORY/TEST/ACCESSIBLE", CancellationToken.None); + + Assert.Equal(DateTime.MinValue, begin); + Assert.Equal(DateTime.MaxValue, end); + } + + [Fact] + public async Task CanProvideAvailability() + { + var dataSource = new Sample() as IDataSource; + + var context = new DataSourceContext( + ResourceLocator: default, + SystemConfiguration: default!, + SourceConfiguration: default!, + RequestConfiguration: default); + + await dataSource.SetContextAsync(context, NullLogger.Instance, CancellationToken.None); + + var begin = new DateTime(2020, 01, 02, 00, 00, 00, DateTimeKind.Utc); + var end = new DateTime(2020, 01, 03, 00, 00, 00, DateTimeKind.Utc); + var expected = 1; + var actual = await dataSource.GetAvailabilityAsync("/A/B/C", begin, end, CancellationToken.None); + + Assert.Equal(expected, actual); + } + + [Fact] + public async Task CanReadFullDay() { - [Fact] - public async Task ProvidesCatalog() - { - // arrange - var dataSource = new Sample() as IDataSource; - - var context = new DataSourceContext( - ResourceLocator: default, - SystemConfiguration: default!, - SourceConfiguration: default!, - RequestConfiguration: default); - - await dataSource.SetContextAsync(context, NullLogger.Instance, CancellationToken.None); - - // act - var actual = await dataSource.GetCatalogAsync(Sample.LocalCatalogId, CancellationToken.None); - - // assert - var actualIds = actual.Resources!.Select(resource => resource.Id).ToList(); - var actualUnits = actual.Resources!.Select(resource => resource.Properties?.GetStringValue("unit")).ToList(); - var actualGroups = actual.Resources!.SelectMany(resource => resource.Properties?.GetStringArray("groups") ?? Array.Empty()); - var actualDataTypes = actual.Resources!.SelectMany(resource => resource.Representations!.Select(representation => representation.DataType)).ToList(); - - var expectedIds = new List() { "T1", "V1", "unix_time1", "unix_time2" }; - var expectedUnits = new List() { "°C", "m/s", default!, default! }; - var expectedGroups = new List() { "Group 1", "Group 1", "Group 2", "Group 2" }; - var expectedDataTypes = new List() { NexusDataType.FLOAT64, NexusDataType.FLOAT64, NexusDataType.FLOAT64, NexusDataType.FLOAT64 }; - - Assert.True(expectedIds.SequenceEqual(actualIds)); - Assert.True(expectedUnits.SequenceEqual(actualUnits)); - Assert.True(expectedGroups.SequenceEqual(actualGroups)); - Assert.True(expectedDataTypes.SequenceEqual(actualDataTypes)); - } - - [Fact] - public async Task CanProvideTimeRange() - { - var dataSource = new Sample() as IDataSource; - - var context = new DataSourceContext( - ResourceLocator: default, - SystemConfiguration: default!, - SourceConfiguration: default!, - RequestConfiguration: default); - - await dataSource.SetContextAsync(context, NullLogger.Instance, CancellationToken.None); - - var (begin, end) = await dataSource.GetTimeRangeAsync("/IN_MEMORY/TEST/ACCESSIBLE", CancellationToken.None); - - Assert.Equal(DateTime.MinValue, begin); - Assert.Equal(DateTime.MaxValue, end); - } - - [Fact] - public async Task CanProvideAvailability() - { - var dataSource = new Sample() as IDataSource; - - var context = new DataSourceContext( - ResourceLocator: default, - SystemConfiguration: default!, - SourceConfiguration: default!, - RequestConfiguration: default); - - await dataSource.SetContextAsync(context, NullLogger.Instance, CancellationToken.None); - - var begin = new DateTime(2020, 01, 02, 00, 00, 00, DateTimeKind.Utc); - var end = new DateTime(2020, 01, 03, 00, 00, 00, DateTimeKind.Utc); - var expected = 1; - var actual = await dataSource.GetAvailabilityAsync("/A/B/C", begin, end, CancellationToken.None); - - Assert.Equal(expected, actual); - } - - [Fact] - public async Task CanReadFullDay() - { - var dataSource = new Sample() as IDataSource; - - var context = new DataSourceContext( - ResourceLocator: default, - SystemConfiguration: default!, - SourceConfiguration: default!, - RequestConfiguration: default); - - await dataSource.SetContextAsync(context, NullLogger.Instance, CancellationToken.None); - - var catalog = await dataSource.GetCatalogAsync(Sample.LocalCatalogId, CancellationToken.None); - var resource = catalog.Resources![0]; - var representation = resource.Representations![0]; - var catalogItem = new CatalogItem(catalog, resource, representation, Parameters: default); - - var begin = new DateTime(2020, 01, 01, 0, 0, 0, DateTimeKind.Utc); - var end = new DateTime(2020, 01, 02, 0, 0, 0, DateTimeKind.Utc); - var (data, status) = ExtensibilityUtilities.CreateBuffers(representation, begin, end); - - var request = new ReadRequest(catalogItem, data, status); - - await dataSource.ReadAsync( - begin, - end, - new[] { request }, - default!, - new Progress(), - CancellationToken.None); - - var doubleData = data.Cast(); - - Assert.Equal(6.5, doubleData.Span[0], precision: 1); - Assert.Equal(7.9, doubleData.Span[29], precision: 1); - Assert.Equal(6.0, doubleData.Span[54], precision: 1); - } + var dataSource = new Sample() as IDataSource; + + var context = new DataSourceContext( + ResourceLocator: default, + SystemConfiguration: default!, + SourceConfiguration: default!, + RequestConfiguration: default); + + await dataSource.SetContextAsync(context, NullLogger.Instance, CancellationToken.None); + + var catalog = await dataSource.GetCatalogAsync(Sample.LocalCatalogId, CancellationToken.None); + var resource = catalog.Resources![0]; + var representation = resource.Representations![0]; + var catalogItem = new CatalogItem(catalog, resource, representation, Parameters: default); + + var begin = new DateTime(2020, 01, 01, 0, 0, 0, DateTimeKind.Utc); + var end = new DateTime(2020, 01, 02, 0, 0, 0, DateTimeKind.Utc); + var (data, status) = ExtensibilityUtilities.CreateBuffers(representation, begin, end); + + var request = new ReadRequest(catalogItem, data, status); + + await dataSource.ReadAsync( + begin, + end, + new[] { request }, + default!, + new Progress(), + CancellationToken.None); + + var doubleData = data.Cast(); + + Assert.Equal(6.5, doubleData.Span[0], precision: 1); + Assert.Equal(7.9, doubleData.Span[29], precision: 1); + Assert.Equal(6.0, doubleData.Span[54], precision: 1); } } \ No newline at end of file diff --git a/tests/Nexus.Tests/DataWriter/CsvDataWriterTests.cs b/tests/Nexus.Tests/DataWriter/CsvDataWriterTests.cs index b7872ff4..4cb0eb2a 100644 --- a/tests/Nexus.Tests/DataWriter/CsvDataWriterTests.cs +++ b/tests/Nexus.Tests/DataWriter/CsvDataWriterTests.cs @@ -8,180 +8,179 @@ using System.Text.Json; using Xunit; -namespace DataWriter +namespace DataWriter; + +public class CsvDataWriterTests : IClassFixture { - public class CsvDataWriterTests : IClassFixture + private readonly DataWriterFixture _fixture; + + public CsvDataWriterTests(DataWriterFixture fixture) { - private readonly DataWriterFixture _fixture; + _fixture = fixture; + } - public CsvDataWriterTests(DataWriterFixture fixture) - { - _fixture = fixture; - } + [Theory] + [InlineData("index")] + [InlineData("unix")] + [InlineData("excel")] + [InlineData("iso-8601")] + public async Task CanWriteFiles(string rowIndexFormat) + { + var targetFolder = _fixture.GetTargetFolder(); + using var dataWriter = new Csv(); - [Theory] - [InlineData("index")] - [InlineData("unix")] - [InlineData("excel")] - [InlineData("iso-8601")] - public async Task CanWriteFiles(string rowIndexFormat) - { - var targetFolder = _fixture.GetTargetFolder(); - using var dataWriter = new Csv(); + var context = new DataWriterContext( + ResourceLocator: new Uri(targetFolder), + SystemConfiguration: default!, + RequestConfiguration: new Dictionary + { + ["row-index-format"] = JsonSerializer.SerializeToElement(rowIndexFormat), + ["significant-figures"] = JsonSerializer.SerializeToElement("7") + }); - var context = new DataWriterContext( - ResourceLocator: new Uri(targetFolder), - SystemConfiguration: default!, - RequestConfiguration: new Dictionary - { - ["row-index-format"] = JsonSerializer.SerializeToElement(rowIndexFormat), - ["significant-figures"] = JsonSerializer.SerializeToElement("7") - }); + await dataWriter.SetContextAsync(context, NullLogger.Instance, CancellationToken.None); - await dataWriter.SetContextAsync(context, NullLogger.Instance, CancellationToken.None); + var begin = new DateTime(2020, 01, 01, 0, 0, 0, DateTimeKind.Utc); + var samplePeriod = TimeSpan.FromSeconds(1); - var begin = new DateTime(2020, 01, 01, 0, 0, 0, DateTimeKind.Utc); - var samplePeriod = TimeSpan.FromSeconds(1); + var catalogItems = _fixture.Catalogs.SelectMany(catalog => (catalog.Resources ?? throw new Exception("resource is null")) + .SelectMany(resource => (resource.Representations ?? throw new Exception("representations is null")) + .Select(representation => new CatalogItem(catalog, resource, representation, Parameters: default)))) + .ToArray(); - var catalogItems = _fixture.Catalogs.SelectMany(catalog => (catalog.Resources ?? throw new Exception("resource is null")) - .SelectMany(resource => (resource.Representations ?? throw new Exception("representations is null")) - .Select(representation => new CatalogItem(catalog, resource, representation, Parameters: default)))) - .ToArray(); + var random = new Random(Seed: 1); + var length = 1000; - var random = new Random(Seed: 1); - var length = 1000; + var data = new[] + { + Enumerable + .Range(0, length) + .Select(value => random.NextDouble() * 1e4) + .ToArray(), + + Enumerable + .Range(0, length) + .Select(value => random.NextDouble() * -1) + .ToArray(), + + Enumerable + .Range(0, length) + .Select(value => random.NextDouble() * Math.PI) + .ToArray() + }; + + var requests = catalogItems + .Select((catalogItem, i) => new WriteRequest(catalogItem, data[i])) + .ToArray(); + + await dataWriter.OpenAsync(begin, default, samplePeriod, catalogItems, CancellationToken.None); + await dataWriter.WriteAsync(TimeSpan.Zero, requests, new Progress(), CancellationToken.None); + await dataWriter.WriteAsync(TimeSpan.FromSeconds(length), requests, new Progress(), CancellationToken.None); + await dataWriter.CloseAsync(CancellationToken.None); + + dataWriter.Dispose(); + + var actualFilePaths = Directory + .GetFiles(targetFolder, "*.csv") + .OrderBy(value => value) + .ToArray(); + + var nfi = new NumberFormatInfo() + { + NumberDecimalSeparator = ".", + NumberGroupSeparator = string.Empty + }; - var data = new[] - { - Enumerable - .Range(0, length) - .Select(value => random.NextDouble() * 1e4) - .ToArray(), - - Enumerable - .Range(0, length) - .Select(value => random.NextDouble() * -1) - .ToArray(), - - Enumerable - .Range(0, length) - .Select(value => random.NextDouble() * Math.PI) - .ToArray() - }; - - var requests = catalogItems - .Select((catalogItem, i) => new WriteRequest(catalogItem, data[i])) - .ToArray(); - - await dataWriter.OpenAsync(begin, default, samplePeriod, catalogItems, CancellationToken.None); - await dataWriter.WriteAsync(TimeSpan.Zero, requests, new Progress(), CancellationToken.None); - await dataWriter.WriteAsync(TimeSpan.FromSeconds(length), requests, new Progress(), CancellationToken.None); - await dataWriter.CloseAsync(CancellationToken.None); - - dataWriter.Dispose(); - - var actualFilePaths = Directory - .GetFiles(targetFolder, "*.csv") - .OrderBy(value => value) - .ToArray(); - - var nfi = new NumberFormatInfo() + var expected = Enumerable + .Range(0, 4) + .Select(value => { - NumberDecimalSeparator = ".", - NumberGroupSeparator = string.Empty - }; - - var expected = Enumerable - .Range(0, 4) - .Select(value => + return rowIndexFormat switch { - return rowIndexFormat switch - { - "index" => ("Index", "1999", string.Format(nfi, "{0:N0}", value)), - "unix" => ("Unix time", "1577838799.00000", string.Format(nfi, "{0:N5}", (begin.AddSeconds(value) - new DateTime(1970, 01, 01)).TotalSeconds)), - "excel" => ("Excel time", "43831.023136574", string.Format(nfi, "{0:N9}", begin.AddSeconds(value).ToOADate())), - "iso-8601" => ("ISO 8601 time", "2020-01-01T00:33:19.0000000Z", begin.AddSeconds(value).ToString("o")), - _ => throw new Exception($"Row index format {rowIndexFormat} is not supported.") - }; - }) - .ToArray(); - - // assert - var startInfo = new ProcessStartInfo - { - CreateNoWindow = true, - FileName = "frictionless", - Arguments = $"validate {targetFolder}/A_B_C_1_s.resource.json", - RedirectStandardOutput = true, - }; + "index" => ("Index", "1999", string.Format(nfi, "{0:N0}", value)), + "unix" => ("Unix time", "1577838799.00000", string.Format(nfi, "{0:N5}", (begin.AddSeconds(value) - new DateTime(1970, 01, 01)).TotalSeconds)), + "excel" => ("Excel time", "43831.023136574", string.Format(nfi, "{0:N9}", begin.AddSeconds(value).ToOADate())), + "iso-8601" => ("ISO 8601 time", "2020-01-01T00:33:19.0000000Z", begin.AddSeconds(value).ToString("o")), + _ => throw new Exception($"Row index format {rowIndexFormat} is not supported.") + }; + }) + .ToArray(); + + // assert + var startInfo = new ProcessStartInfo + { + CreateNoWindow = true, + FileName = "frictionless", + Arguments = $"validate {targetFolder}/A_B_C_1_s.resource.json", + RedirectStandardOutput = true, + }; - using (var process = Process.Start(startInfo)) - { - if (process is null) - throw new Exception("process is null"); + using (var process = Process.Start(startInfo)) + { + if (process is null) + throw new Exception("process is null"); - process.WaitForExit(); - Assert.Equal(0, process.ExitCode); - } + process.WaitForExit(); + Assert.Equal(0, process.ExitCode); + } - startInfo.Arguments = $"validate {targetFolder}/D_E_F_1_s.resource.json"; + startInfo.Arguments = $"validate {targetFolder}/D_E_F_1_s.resource.json"; - using (var process = Process.Start(startInfo)) - { - if (process is null) - throw new Exception("process is null"); + using (var process = Process.Start(startInfo)) + { + if (process is null) + throw new Exception("process is null"); - process.WaitForExit(); - Assert.Equal(0, process.ExitCode); - } + process.WaitForExit(); + Assert.Equal(0, process.ExitCode); + } - // assert /A/B/C - var expectedLines1 = new[] - { - "# date_time: 2020-01-01T00-00-00Z", - "# sample_period: 1_s", - "# catalog_id: /A/B/C", - $"{expected[0].Item1},resource1_1_s (°C),resource1_10_s (°C)", - $"{expected[0].Item3},2486.686,-0.7557958", - $"{expected[1].Item3},1107.44,-0.4584072", - $"{expected[2].Item3},4670.107,-0.001267695", - $"{expected[3].Item3},7716.041,-0.09289372" - }; - - var actualLines1 = File.ReadLines(actualFilePaths[0], Encoding.UTF8).ToList(); - - Assert.Equal("A_B_C_2020-01-01T00-00-00Z_1_s.csv", Path.GetFileName(actualFilePaths[0])); - Assert.Equal($"{expected[0].Item2},412.6589,-0.7542502", actualLines1.Last()); - Assert.Equal(2004, actualLines1.Count); - - foreach (var (expectedLine, actualLine) in expectedLines1.Zip(actualLines1.Take(14))) - { - Assert.Equal(expectedLine, actualLine); - } + // assert /A/B/C + var expectedLines1 = new[] + { + "# date_time: 2020-01-01T00-00-00Z", + "# sample_period: 1_s", + "# catalog_id: /A/B/C", + $"{expected[0].Item1},resource1_1_s (°C),resource1_10_s (°C)", + $"{expected[0].Item3},2486.686,-0.7557958", + $"{expected[1].Item3},1107.44,-0.4584072", + $"{expected[2].Item3},4670.107,-0.001267695", + $"{expected[3].Item3},7716.041,-0.09289372" + }; + + var actualLines1 = File.ReadLines(actualFilePaths[0], Encoding.UTF8).ToList(); + + Assert.Equal("A_B_C_2020-01-01T00-00-00Z_1_s.csv", Path.GetFileName(actualFilePaths[0])); + Assert.Equal($"{expected[0].Item2},412.6589,-0.7542502", actualLines1.Last()); + Assert.Equal(2004, actualLines1.Count); + + foreach (var (expectedLine, actualLine) in expectedLines1.Zip(actualLines1.Take(14))) + { + Assert.Equal(expectedLine, actualLine); + } - // assert /D/E/F - var expectedLines2 = new[] - { - "# date_time: 2020-01-01T00-00-00Z", - "# sample_period: 1_s", - "# catalog_id: /D/E/F", - $"{expected[0].Item1},resource3_1_s (m/s)", - $"{expected[0].Item3},1.573993", - $"{expected[1].Item3},0.4618637", - $"{expected[2].Item3},1.094448", - $"{expected[3].Item3},2.758635" - }; - - var actualLines2 = File.ReadLines(actualFilePaths[1], Encoding.UTF8).ToList(); - - Assert.Equal("D_E_F_2020-01-01T00-00-00Z_1_s.csv", Path.GetFileName(actualFilePaths[1])); - Assert.Equal($"{expected[0].Item2},2.336974", actualLines2.Last()); - Assert.Equal(2004, actualLines2.Count); - - foreach (var (expectedLine, actualLine) in expectedLines2.Zip(actualLines2.Take(13))) - { - Assert.Equal(expectedLine, actualLine); - } + // assert /D/E/F + var expectedLines2 = new[] + { + "# date_time: 2020-01-01T00-00-00Z", + "# sample_period: 1_s", + "# catalog_id: /D/E/F", + $"{expected[0].Item1},resource3_1_s (m/s)", + $"{expected[0].Item3},1.573993", + $"{expected[1].Item3},0.4618637", + $"{expected[2].Item3},1.094448", + $"{expected[3].Item3},2.758635" + }; + + var actualLines2 = File.ReadLines(actualFilePaths[1], Encoding.UTF8).ToList(); + + Assert.Equal("D_E_F_2020-01-01T00-00-00Z_1_s.csv", Path.GetFileName(actualFilePaths[1])); + Assert.Equal($"{expected[0].Item2},2.336974", actualLines2.Last()); + Assert.Equal(2004, actualLines2.Count); + + foreach (var (expectedLine, actualLine) in expectedLines2.Zip(actualLines2.Take(13))) + { + Assert.Equal(expectedLine, actualLine); } } } \ No newline at end of file diff --git a/tests/Nexus.Tests/DataWriter/DataWriterControllerTests.cs b/tests/Nexus.Tests/DataWriter/DataWriterControllerTests.cs index d68e5029..c08ef818 100644 --- a/tests/Nexus.Tests/DataWriter/DataWriterControllerTests.cs +++ b/tests/Nexus.Tests/DataWriter/DataWriterControllerTests.cs @@ -6,168 +6,167 @@ using System.IO.Pipelines; using Xunit; -namespace DataWriter +namespace DataWriter; + +public class DataWriterControllerTests : IClassFixture { - public class DataWriterControllerTests : IClassFixture + private readonly DataWriterFixture _fixture; + + public DataWriterControllerTests(DataWriterFixture fixture) { - private readonly DataWriterFixture _fixture; + _fixture = fixture; + } - public DataWriterControllerTests(DataWriterFixture fixture) - { - _fixture = fixture; - } + [Fact] + public async Task CanWrite() + { + // prepare write + var begin = new DateTime(2020, 01, 01, 1, 0, 0, DateTimeKind.Utc); + var end = new DateTime(2020, 01, 01, 3, 0, 0, DateTimeKind.Utc); + var samplePeriod = TimeSpan.FromMinutes(10); + var filePeriod = TimeSpan.FromMinutes(30); + + var catalogItems = _fixture.Catalogs + .SelectMany(catalog => catalog.Resources! + .SelectMany(resource => resource.Representations! + .Select(representation => new CatalogItem( + catalog, + resource, + new Representation(representation.DataType, samplePeriod: TimeSpan.FromMinutes(10)), + Parameters: default)))) + .ToArray(); + + var catalogItemRequests = catalogItems + .Select(catalogItem => new CatalogItemRequest(catalogItem, default, default!)) + .ToArray(); + + var pipes = catalogItemRequests + .Select(catalogItemRequest => new Pipe()) + .ToArray(); + + var catalogItemRequestPipeReaders = catalogItemRequests + .Zip(pipes) + .Select((value) => new CatalogItemRequestPipeReader(value.First, value.Second.Reader)) + .ToArray(); + + var random = new Random(Seed: 1); + var totalLength = (end - begin).Ticks / samplePeriod.Ticks; + + var expectedDatasets = pipes + .Select(pipe => Enumerable.Range(0, (int)totalLength).Select(value => random.NextDouble()).ToArray()) + .ToArray(); + + var actualDatasets = pipes + .Select(pipe => Enumerable.Range(0, (int)totalLength).Select(value => 0.0).ToArray()) + .ToArray(); + + // mock IDataWriter + var dataWriter = Mock.Of(); + + var fileNo = -1; + + Mock.Get(dataWriter) + .Setup(s => s.OpenAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Callback((_, _, _, _, _) => + { + fileNo++; + }); - [Fact] - public async Task CanWrite() - { - // prepare write - var begin = new DateTime(2020, 01, 01, 1, 0, 0, DateTimeKind.Utc); - var end = new DateTime(2020, 01, 01, 3, 0, 0, DateTimeKind.Utc); - var samplePeriod = TimeSpan.FromMinutes(10); - var filePeriod = TimeSpan.FromMinutes(30); - - var catalogItems = _fixture.Catalogs - .SelectMany(catalog => catalog.Resources! - .SelectMany(resource => resource.Representations! - .Select(representation => new CatalogItem( - catalog, - resource, - new Representation(representation.DataType, samplePeriod: TimeSpan.FromMinutes(10)), - Parameters: default)))) - .ToArray(); - - var catalogItemRequests = catalogItems - .Select(catalogItem => new CatalogItemRequest(catalogItem, default, default!)) - .ToArray(); - - var pipes = catalogItemRequests - .Select(catalogItemRequest => new Pipe()) - .ToArray(); - - var catalogItemRequestPipeReaders = catalogItemRequests - .Zip(pipes) - .Select((value) => new CatalogItemRequestPipeReader(value.First, value.Second.Reader)) - .ToArray(); - - var random = new Random(Seed: 1); - var totalLength = (end - begin).Ticks / samplePeriod.Ticks; - - var expectedDatasets = pipes - .Select(pipe => Enumerable.Range(0, (int)totalLength).Select(value => random.NextDouble()).ToArray()) - .ToArray(); - - var actualDatasets = pipes - .Select(pipe => Enumerable.Range(0, (int)totalLength).Select(value => 0.0).ToArray()) - .ToArray(); - - // mock IDataWriter - var dataWriter = Mock.Of(); - - var fileNo = -1; - - Mock.Get(dataWriter) - .Setup(s => s.OpenAsync( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny())) - .Callback((_, _, _, _, _) => - { - fileNo++; - }); - - Mock.Get(dataWriter) - .Setup(s => s.WriteAsync( - It.IsAny(), - It.IsAny(), - It.IsAny>(), - It.IsAny()) - ) - .Callback, CancellationToken>((fileOffset, requests, progress, cancellationToken) => + Mock.Get(dataWriter) + .Setup(s => s.WriteAsync( + It.IsAny(), + It.IsAny(), + It.IsAny>(), + It.IsAny()) + ) + .Callback, CancellationToken>((fileOffset, requests, progress, cancellationToken) => + { + var fileLength = (int)(filePeriod.Ticks / samplePeriod.Ticks); + var fileElementOffset = (int)(fileOffset.Ticks / samplePeriod.Ticks); + + foreach (var ((catalogItem, source), target) in requests.Zip(actualDatasets)) { - var fileLength = (int)(filePeriod.Ticks / samplePeriod.Ticks); - var fileElementOffset = (int)(fileOffset.Ticks / samplePeriod.Ticks); + source.Span.CopyTo(target.AsSpan(fileElementOffset + fileNo * fileLength)); + } + }) + .Returns(Task.CompletedTask); - foreach (var ((catalogItem, source), target) in requests.Zip(actualDatasets)) - { - source.Span.CopyTo(target.AsSpan(fileElementOffset + fileNo * fileLength)); - } - }) - .Returns(Task.CompletedTask); + // instantiate controller + var resourceLocator = new Uri("file:///empty"); - // instantiate controller - var resourceLocator = new Uri("file:///empty"); + var controller = new DataWriterController( + dataWriter, + resourceLocator, + default!, + default!, + NullLogger.Instance); - var controller = new DataWriterController( - dataWriter, - resourceLocator, - default!, - default!, - NullLogger.Instance); + await controller.InitializeAsync(default!, CancellationToken.None); - await controller.InitializeAsync(default!, CancellationToken.None); + // read data + var chunkSize = 2; - // read data - var chunkSize = 2; + var reading = Task.Run(async () => + { + var remaining = totalLength; + var offset = 0; - var reading = Task.Run(async () => + while (remaining > 0) { - var remaining = totalLength; - var offset = 0; + var currentChunk = (int)Math.Min(remaining, chunkSize); - while (remaining > 0) + foreach (var (pipe, dataset) in pipes.Zip(expectedDatasets)) { - var currentChunk = (int)Math.Min(remaining, chunkSize); - - foreach (var (pipe, dataset) in pipes.Zip(expectedDatasets)) - { - var buffer = dataset - .AsMemory() - .Slice(offset, currentChunk) - .Cast(); + var buffer = dataset + .AsMemory() + .Slice(offset, currentChunk) + .Cast(); - await pipe.Writer.WriteAsync(buffer); - } - - remaining -= currentChunk; - offset += currentChunk; + await pipe.Writer.WriteAsync(buffer); } - foreach (var pipe in pipes) - { - await pipe.Writer.CompleteAsync(); - } - }); + remaining -= currentChunk; + offset += currentChunk; + } - // write data - var writing = controller.WriteAsync(begin, end, samplePeriod, filePeriod, catalogItemRequestPipeReaders, default, CancellationToken.None); + foreach (var pipe in pipes) + { + await pipe.Writer.CompleteAsync(); + } + }); - // wait for completion - await Task.WhenAll(writing, reading); + // write data + var writing = controller.WriteAsync(begin, end, samplePeriod, filePeriod, catalogItemRequestPipeReaders, default, CancellationToken.None); - // assert - var begin1 = new DateTime(2020, 01, 01, 1, 0, 0, DateTimeKind.Utc); - Mock.Get(dataWriter).Verify(dataWriter => dataWriter.OpenAsync(begin1, filePeriod, samplePeriod, catalogItems, default), Times.Once); + // wait for completion + await Task.WhenAll(writing, reading); - var begin2 = new DateTime(2020, 01, 01, 1, 30, 0, DateTimeKind.Utc); - Mock.Get(dataWriter).Verify(dataWriter => dataWriter.OpenAsync(begin2, filePeriod, samplePeriod, catalogItems, default), Times.Once); + // assert + var begin1 = new DateTime(2020, 01, 01, 1, 0, 0, DateTimeKind.Utc); + Mock.Get(dataWriter).Verify(dataWriter => dataWriter.OpenAsync(begin1, filePeriod, samplePeriod, catalogItems, default), Times.Once); - var begin3 = new DateTime(2020, 01, 01, 2, 0, 0, DateTimeKind.Utc); - Mock.Get(dataWriter).Verify(dataWriter => dataWriter.OpenAsync(begin3, filePeriod, samplePeriod, catalogItems, default), Times.Once); + var begin2 = new DateTime(2020, 01, 01, 1, 30, 0, DateTimeKind.Utc); + Mock.Get(dataWriter).Verify(dataWriter => dataWriter.OpenAsync(begin2, filePeriod, samplePeriod, catalogItems, default), Times.Once); - var begin4 = new DateTime(2020, 01, 01, 2, 30, 0, DateTimeKind.Utc); - Mock.Get(dataWriter).Verify(dataWriter => dataWriter.OpenAsync(begin4, filePeriod, samplePeriod, catalogItems, default), Times.Once); + var begin3 = new DateTime(2020, 01, 01, 2, 0, 0, DateTimeKind.Utc); + Mock.Get(dataWriter).Verify(dataWriter => dataWriter.OpenAsync(begin3, filePeriod, samplePeriod, catalogItems, default), Times.Once); - var begin5 = new DateTime(2020, 01, 01, 3, 00, 0, DateTimeKind.Utc); - Mock.Get(dataWriter).Verify(dataWriter => dataWriter.OpenAsync(begin5, filePeriod, samplePeriod, catalogItems, default), Times.Never); + var begin4 = new DateTime(2020, 01, 01, 2, 30, 0, DateTimeKind.Utc); + Mock.Get(dataWriter).Verify(dataWriter => dataWriter.OpenAsync(begin4, filePeriod, samplePeriod, catalogItems, default), Times.Once); - Mock.Get(dataWriter).Verify(dataWriter => dataWriter.CloseAsync(default), Times.Exactly(4)); + var begin5 = new DateTime(2020, 01, 01, 3, 00, 0, DateTimeKind.Utc); + Mock.Get(dataWriter).Verify(dataWriter => dataWriter.OpenAsync(begin5, filePeriod, samplePeriod, catalogItems, default), Times.Never); - foreach (var (expected, actual) in expectedDatasets.Zip(actualDatasets)) - { - Assert.True(expected.SequenceEqual(actual)); - } + Mock.Get(dataWriter).Verify(dataWriter => dataWriter.CloseAsync(default), Times.Exactly(4)); + + foreach (var (expected, actual) in expectedDatasets.Zip(actualDatasets)) + { + Assert.True(expected.SequenceEqual(actual)); } } } diff --git a/tests/Nexus.Tests/DataWriter/DataWriterFixture.cs b/tests/Nexus.Tests/DataWriter/DataWriterFixture.cs index 48be2970..12decb9d 100644 --- a/tests/Nexus.Tests/DataWriter/DataWriterFixture.cs +++ b/tests/Nexus.Tests/DataWriter/DataWriterFixture.cs @@ -1,68 +1,67 @@ using Nexus.DataModel; -namespace DataWriter +namespace DataWriter; + +public class DataWriterFixture : IDisposable { - public class DataWriterFixture : IDisposable - { - readonly List _targetFolders = new(); + readonly List _targetFolders = new(); - public DataWriterFixture() + public DataWriterFixture() + { + // catalog 1 + var representations1 = new List() { - // catalog 1 - var representations1 = new List() - { - new Representation(dataType: NexusDataType.FLOAT32, samplePeriod: TimeSpan.FromSeconds(1)), - new Representation(dataType: NexusDataType.FLOAT64, samplePeriod: TimeSpan.FromSeconds(10)), - }; + new Representation(dataType: NexusDataType.FLOAT32, samplePeriod: TimeSpan.FromSeconds(1)), + new Representation(dataType: NexusDataType.FLOAT64, samplePeriod: TimeSpan.FromSeconds(10)), + }; - var resourceBuilder1 = new ResourceBuilder(id: "resource1") - .WithUnit("°C") - .WithGroups("group1") - .AddRepresentations(representations1); + var resourceBuilder1 = new ResourceBuilder(id: "resource1") + .WithUnit("°C") + .WithGroups("group1") + .AddRepresentations(representations1); - var catalogBuilder1 = new ResourceCatalogBuilder(id: "/A/B/C") - .WithProperty("my-custom-parameter1", "my-custom-value1") - .WithProperty("my-custom-parameter2", "my-custom-value2") - .AddResource(resourceBuilder1.Build()); + var catalogBuilder1 = new ResourceCatalogBuilder(id: "/A/B/C") + .WithProperty("my-custom-parameter1", "my-custom-value1") + .WithProperty("my-custom-parameter2", "my-custom-value2") + .AddResource(resourceBuilder1.Build()); - // catalog 2 - var representation2 = new Representation(dataType: NexusDataType.INT64, samplePeriod: TimeSpan.FromSeconds(1)); + // catalog 2 + var representation2 = new Representation(dataType: NexusDataType.INT64, samplePeriod: TimeSpan.FromSeconds(1)); - var resourceBuilder2 = new ResourceBuilder(id: "resource3") - .WithUnit("m/s") - .WithGroups("group2") - .AddRepresentation(representation2); + var resourceBuilder2 = new ResourceBuilder(id: "resource3") + .WithUnit("m/s") + .WithGroups("group2") + .AddRepresentation(representation2); - var catalogBuilder2 = new ResourceCatalogBuilder(id: "/D/E/F") - .WithProperty("my-custom-parameter3", "my-custom-value3") - .AddResource(resourceBuilder2.Build()); + var catalogBuilder2 = new ResourceCatalogBuilder(id: "/D/E/F") + .WithProperty("my-custom-parameter3", "my-custom-value3") + .AddResource(resourceBuilder2.Build()); - Catalogs = new[] { catalogBuilder1.Build(), catalogBuilder2.Build() }; - } + Catalogs = new[] { catalogBuilder1.Build(), catalogBuilder2.Build() }; + } - public ResourceCatalog[] Catalogs { get; } + public ResourceCatalog[] Catalogs { get; } - public string GetTargetFolder() - { - var targetFolder = Path.Combine(Path.GetTempPath(), $"Nexus.Tests.{Guid.NewGuid()}"); - Directory.CreateDirectory(targetFolder); + public string GetTargetFolder() + { + var targetFolder = Path.Combine(Path.GetTempPath(), $"Nexus.Tests.{Guid.NewGuid()}"); + Directory.CreateDirectory(targetFolder); - _targetFolders.Add(targetFolder); - return targetFolder; - } + _targetFolders.Add(targetFolder); + return targetFolder; + } - public void Dispose() + public void Dispose() + { + foreach (var targetFolder in _targetFolders) { - foreach (var targetFolder in _targetFolders) + try + { + Directory.Delete(targetFolder, true); + } + catch { - try - { - Directory.Delete(targetFolder, true); - } - catch - { - // - } + // } } } diff --git a/tests/Nexus.Tests/Other/CacheEntryWrapperTests.cs b/tests/Nexus.Tests/Other/CacheEntryWrapperTests.cs index 81b43dac..872c821b 100644 --- a/tests/Nexus.Tests/Other/CacheEntryWrapperTests.cs +++ b/tests/Nexus.Tests/Other/CacheEntryWrapperTests.cs @@ -1,204 +1,203 @@ using Nexus.Core; using Xunit; -namespace Other +namespace Other; + +public class CacheEntryWrapperTests { - public class CacheEntryWrapperTests + [Fact] + public async Task CanRead() { - [Fact] - public async Task CanRead() - { - // Arrange - var expected = new double[] { 0, 2.2, 3.3, 4.4, 0, 6.6 }; + // Arrange + var expected = new double[] { 0, 2.2, 3.3, 4.4, 0, 6.6 }; - var fileBegin = new DateTime(2020, 01, 01, 0, 0, 0, DateTimeKind.Utc); - var filePeriod = TimeSpan.FromDays(1); - var samplePeriod = TimeSpan.FromHours(3); + var fileBegin = new DateTime(2020, 01, 01, 0, 0, 0, DateTimeKind.Utc); + var filePeriod = TimeSpan.FromDays(1); + var samplePeriod = TimeSpan.FromHours(3); - var cachedIntervals = new[] - { - new Interval(new DateTime(2020, 01, 01, 6, 0, 0, DateTimeKind.Utc), new DateTime(2020, 01, 01, 15, 0, 0, DateTimeKind.Utc)), - new Interval(new DateTime(2020, 01, 01, 18, 0, 0, DateTimeKind.Utc), new DateTime(2020, 01, 01, 21, 0, 0, DateTimeKind.Utc)) - }; + var cachedIntervals = new[] + { + new Interval(new DateTime(2020, 01, 01, 6, 0, 0, DateTimeKind.Utc), new DateTime(2020, 01, 01, 15, 0, 0, DateTimeKind.Utc)), + new Interval(new DateTime(2020, 01, 01, 18, 0, 0, DateTimeKind.Utc), new DateTime(2020, 01, 01, 21, 0, 0, DateTimeKind.Utc)) + }; - var stream = new MemoryStream(); - var writer = new BinaryWriter(stream); + var stream = new MemoryStream(); + var writer = new BinaryWriter(stream); - for (int i = 0; i < 8; i++) - { - writer.Write(i * 1.1); - } + for (int i = 0; i < 8; i++) + { + writer.Write(i * 1.1); + } - CacheEntryWrapper.WriteCachedIntervals(stream, cachedIntervals); + CacheEntryWrapper.WriteCachedIntervals(stream, cachedIntervals); - var wrapper = new CacheEntryWrapper(fileBegin, filePeriod, samplePeriod, stream); + var wrapper = new CacheEntryWrapper(fileBegin, filePeriod, samplePeriod, stream); - var begin = new DateTime(2020, 01, 01, 3, 0, 0, DateTimeKind.Utc); - var end = new DateTime(2020, 01, 01, 21, 0, 0, DateTimeKind.Utc); - var actual = new double[6]; + var begin = new DateTime(2020, 01, 01, 3, 0, 0, DateTimeKind.Utc); + var end = new DateTime(2020, 01, 01, 21, 0, 0, DateTimeKind.Utc); + var actual = new double[6]; - // Act - var uncachedIntervals = await wrapper.ReadAsync(begin, end, actual, CancellationToken.None); + // Act + var uncachedIntervals = await wrapper.ReadAsync(begin, end, actual, CancellationToken.None); - // Assert - var expected1 = new Interval( - Begin: new DateTime(2020, 01, 01, 3, 0, 0, DateTimeKind.Utc), - End: new DateTime(2020, 01, 01, 6, 0, 0, DateTimeKind.Utc)); + // Assert + var expected1 = new Interval( + Begin: new DateTime(2020, 01, 01, 3, 0, 0, DateTimeKind.Utc), + End: new DateTime(2020, 01, 01, 6, 0, 0, DateTimeKind.Utc)); - var expected2 = new Interval( - Begin: new DateTime(2020, 01, 01, 15, 0, 0, DateTimeKind.Utc), - End: new DateTime(2020, 01, 01, 18, 0, 0, DateTimeKind.Utc)); + var expected2 = new Interval( + Begin: new DateTime(2020, 01, 01, 15, 0, 0, DateTimeKind.Utc), + End: new DateTime(2020, 01, 01, 18, 0, 0, DateTimeKind.Utc)); - Assert.Collection(uncachedIntervals, - actual1 => Assert.Equal(expected1, actual1), - actual2 => Assert.Equal(expected2, actual2)); + Assert.Collection(uncachedIntervals, + actual1 => Assert.Equal(expected1, actual1), + actual2 => Assert.Equal(expected2, actual2)); - Assert.Equal(expected.Length, actual.Length); + Assert.Equal(expected.Length, actual.Length); - for (int i = 0; i < expected.Length; i++) - { - Assert.Equal(expected[i], actual[i], precision: 1); - } + for (int i = 0; i < expected.Length; i++) + { + Assert.Equal(expected[i], actual[i], precision: 1); } + } - [Fact] - public async Task CanWrite1() - { - var fileBegin = new DateTime(2020, 01, 01, 0, 0, 0, DateTimeKind.Utc); - var filePeriod = TimeSpan.FromDays(1); - var samplePeriod = TimeSpan.FromHours(3); + [Fact] + public async Task CanWrite1() + { + var fileBegin = new DateTime(2020, 01, 01, 0, 0, 0, DateTimeKind.Utc); + var filePeriod = TimeSpan.FromDays(1); + var samplePeriod = TimeSpan.FromHours(3); - var stream = new MemoryStream(); + var stream = new MemoryStream(); - var cachedIntervals = new[] - { - new Interval(new DateTime(2020, 01, 01, 6, 0, 0, DateTimeKind.Utc), new DateTime(2020, 01, 01, 15, 0, 0, DateTimeKind.Utc)), - new Interval(new DateTime(2020, 01, 01, 18, 0, 0, DateTimeKind.Utc), new DateTime(2020, 01, 01, 21, 0, 0, DateTimeKind.Utc)) - }; + var cachedIntervals = new[] + { + new Interval(new DateTime(2020, 01, 01, 6, 0, 0, DateTimeKind.Utc), new DateTime(2020, 01, 01, 15, 0, 0, DateTimeKind.Utc)), + new Interval(new DateTime(2020, 01, 01, 18, 0, 0, DateTimeKind.Utc), new DateTime(2020, 01, 01, 21, 0, 0, DateTimeKind.Utc)) + }; - stream.Seek(8 * sizeof(double), SeekOrigin.Begin); - CacheEntryWrapper.WriteCachedIntervals(stream, cachedIntervals); + stream.Seek(8 * sizeof(double), SeekOrigin.Begin); + CacheEntryWrapper.WriteCachedIntervals(stream, cachedIntervals); - // Arrange - var wrapper = new CacheEntryWrapper(fileBegin, filePeriod, samplePeriod, stream); + // Arrange + var wrapper = new CacheEntryWrapper(fileBegin, filePeriod, samplePeriod, stream); - var begin1 = new DateTime(2020, 01, 01, 3, 0, 0, DateTimeKind.Utc); - var sourceBuffer1 = new double[2] { 88.8, 99.9 }; + var begin1 = new DateTime(2020, 01, 01, 3, 0, 0, DateTimeKind.Utc); + var sourceBuffer1 = new double[2] { 88.8, 99.9 }; - var begin2 = new DateTime(2020, 01, 01, 15, 0, 0, DateTimeKind.Utc); - var sourceBuffer2 = new double[1] { 66.6 }; + var begin2 = new DateTime(2020, 01, 01, 15, 0, 0, DateTimeKind.Utc); + var sourceBuffer2 = new double[1] { 66.6 }; - // Act - await wrapper.WriteAsync(begin1, sourceBuffer1, CancellationToken.None); + // Act + await wrapper.WriteAsync(begin1, sourceBuffer1, CancellationToken.None); - stream.Seek(8 * sizeof(double), SeekOrigin.Begin); - var actualIntervals1 = CacheEntryWrapper.ReadCachedIntervals(stream); + stream.Seek(8 * sizeof(double), SeekOrigin.Begin); + var actualIntervals1 = CacheEntryWrapper.ReadCachedIntervals(stream); - await wrapper.WriteAsync(begin2, sourceBuffer2, CancellationToken.None); + await wrapper.WriteAsync(begin2, sourceBuffer2, CancellationToken.None); - stream.Seek(8 * sizeof(double), SeekOrigin.Begin); - var actualIntervals2 = CacheEntryWrapper.ReadCachedIntervals(stream); + stream.Seek(8 * sizeof(double), SeekOrigin.Begin); + var actualIntervals2 = CacheEntryWrapper.ReadCachedIntervals(stream); - // Assert - var reader = new BinaryReader(stream); + // Assert + var reader = new BinaryReader(stream); - var expectedIntervals1 = new[] - { - new Interval( - Begin: new DateTime(2020, 01, 01, 3, 0, 0, DateTimeKind.Utc), - End: new DateTime(2020, 01, 01, 15, 0, 0, DateTimeKind.Utc)), + var expectedIntervals1 = new[] + { + new Interval( + Begin: new DateTime(2020, 01, 01, 3, 0, 0, DateTimeKind.Utc), + End: new DateTime(2020, 01, 01, 15, 0, 0, DateTimeKind.Utc)), - new Interval( - Begin: new DateTime(2020, 01, 01, 18, 0, 0, DateTimeKind.Utc), - End: new DateTime(2020, 01, 01, 21, 0, 0, DateTimeKind.Utc)) - }; + new Interval( + Begin: new DateTime(2020, 01, 01, 18, 0, 0, DateTimeKind.Utc), + End: new DateTime(2020, 01, 01, 21, 0, 0, DateTimeKind.Utc)) + }; - Assert.True(expectedIntervals1.SequenceEqual(actualIntervals1)); + Assert.True(expectedIntervals1.SequenceEqual(actualIntervals1)); - stream.Seek(1 * sizeof(double), SeekOrigin.Begin); - Assert.Equal(88.8, reader.ReadDouble()); - Assert.Equal(99.9, reader.ReadDouble()); + stream.Seek(1 * sizeof(double), SeekOrigin.Begin); + Assert.Equal(88.8, reader.ReadDouble()); + Assert.Equal(99.9, reader.ReadDouble()); - var expectedIntervals2 = new[] - { - new Interval( - Begin: new DateTime(2020, 01, 01, 3, 0, 0, DateTimeKind.Utc), - End: new DateTime(2020, 01, 01, 21, 0, 0, DateTimeKind.Utc)) - }; + var expectedIntervals2 = new[] + { + new Interval( + Begin: new DateTime(2020, 01, 01, 3, 0, 0, DateTimeKind.Utc), + End: new DateTime(2020, 01, 01, 21, 0, 0, DateTimeKind.Utc)) + }; - Assert.True(expectedIntervals2.SequenceEqual(actualIntervals2)); + Assert.True(expectedIntervals2.SequenceEqual(actualIntervals2)); - stream.Seek(5 * sizeof(double), SeekOrigin.Begin); - Assert.Equal(66.6, reader.ReadDouble()); - } + stream.Seek(5 * sizeof(double), SeekOrigin.Begin); + Assert.Equal(66.6, reader.ReadDouble()); + } - [Fact] - public async Task CanWrite2() - { - var fileBegin = new DateTime(2020, 01, 01, 0, 0, 0, DateTimeKind.Utc); - var filePeriod = TimeSpan.FromDays(1); - var samplePeriod = TimeSpan.FromHours(3); + [Fact] + public async Task CanWrite2() + { + var fileBegin = new DateTime(2020, 01, 01, 0, 0, 0, DateTimeKind.Utc); + var filePeriod = TimeSpan.FromDays(1); + var samplePeriod = TimeSpan.FromHours(3); - var stream = new MemoryStream(); + var stream = new MemoryStream(); - var cachedIntervals = new[] - { - new Interval(new DateTime(2020, 01, 01, 0, 0, 0, DateTimeKind.Utc), new DateTime(2020, 01, 01, 6, 0, 0, DateTimeKind.Utc)), - new Interval(new DateTime(2020, 01, 01, 12, 0, 0, DateTimeKind.Utc), new DateTime(2020, 01, 01, 18, 0, 0, DateTimeKind.Utc)) - }; + var cachedIntervals = new[] + { + new Interval(new DateTime(2020, 01, 01, 0, 0, 0, DateTimeKind.Utc), new DateTime(2020, 01, 01, 6, 0, 0, DateTimeKind.Utc)), + new Interval(new DateTime(2020, 01, 01, 12, 0, 0, DateTimeKind.Utc), new DateTime(2020, 01, 01, 18, 0, 0, DateTimeKind.Utc)) + }; - stream.Seek(8 * sizeof(double), SeekOrigin.Begin); - CacheEntryWrapper.WriteCachedIntervals(stream, cachedIntervals); + stream.Seek(8 * sizeof(double), SeekOrigin.Begin); + CacheEntryWrapper.WriteCachedIntervals(stream, cachedIntervals); - // Arrange - var wrapper = new CacheEntryWrapper(fileBegin, filePeriod, samplePeriod, stream); + // Arrange + var wrapper = new CacheEntryWrapper(fileBegin, filePeriod, samplePeriod, stream); - var begin1 = new DateTime(2020, 01, 01, 3, 0, 0, DateTimeKind.Utc); - var sourceBuffer1 = new double[3] { 77.7, 88.8, 99.9 }; + var begin1 = new DateTime(2020, 01, 01, 3, 0, 0, DateTimeKind.Utc); + var sourceBuffer1 = new double[3] { 77.7, 88.8, 99.9 }; - var begin2 = new DateTime(2020, 01, 01, 18, 0, 0, DateTimeKind.Utc); - var sourceBuffer2 = new double[2] { 66.6, 77.7 }; + var begin2 = new DateTime(2020, 01, 01, 18, 0, 0, DateTimeKind.Utc); + var sourceBuffer2 = new double[2] { 66.6, 77.7 }; - // Act - await wrapper.WriteAsync(begin1, sourceBuffer1, CancellationToken.None); + // Act + await wrapper.WriteAsync(begin1, sourceBuffer1, CancellationToken.None); - stream.Seek(8 * sizeof(double), SeekOrigin.Begin); - var actualIntervals1 = CacheEntryWrapper.ReadCachedIntervals(stream); + stream.Seek(8 * sizeof(double), SeekOrigin.Begin); + var actualIntervals1 = CacheEntryWrapper.ReadCachedIntervals(stream); - await wrapper.WriteAsync(begin2, sourceBuffer2, CancellationToken.None); + await wrapper.WriteAsync(begin2, sourceBuffer2, CancellationToken.None); - stream.Seek(8 * sizeof(double), SeekOrigin.Begin); - var actualIntervals2 = CacheEntryWrapper.ReadCachedIntervals(stream); + stream.Seek(8 * sizeof(double), SeekOrigin.Begin); + var actualIntervals2 = CacheEntryWrapper.ReadCachedIntervals(stream); - // Assert - var reader = new BinaryReader(stream); + // Assert + var reader = new BinaryReader(stream); - var expectedIntervals1 = new[] - { - new Interval( - Begin: new DateTime(2020, 01, 01, 0, 0, 0, DateTimeKind.Utc), - End: new DateTime(2020, 01, 01, 18, 0, 0, DateTimeKind.Utc)) - }; + var expectedIntervals1 = new[] + { + new Interval( + Begin: new DateTime(2020, 01, 01, 0, 0, 0, DateTimeKind.Utc), + End: new DateTime(2020, 01, 01, 18, 0, 0, DateTimeKind.Utc)) + }; - Assert.True(expectedIntervals1.SequenceEqual(actualIntervals1)); + Assert.True(expectedIntervals1.SequenceEqual(actualIntervals1)); - stream.Seek(1 * sizeof(double), SeekOrigin.Begin); - Assert.Equal(77.7, reader.ReadDouble()); - Assert.Equal(88.8, reader.ReadDouble()); - Assert.Equal(99.9, reader.ReadDouble()); + stream.Seek(1 * sizeof(double), SeekOrigin.Begin); + Assert.Equal(77.7, reader.ReadDouble()); + Assert.Equal(88.8, reader.ReadDouble()); + Assert.Equal(99.9, reader.ReadDouble()); - var expectedIntervals2 = new[] - { - new Interval( - Begin: new DateTime(2020, 01, 01, 0, 0, 0, DateTimeKind.Utc), - End: new DateTime(2020, 01, 02, 0, 0, 0, DateTimeKind.Utc)) - }; + var expectedIntervals2 = new[] + { + new Interval( + Begin: new DateTime(2020, 01, 01, 0, 0, 0, DateTimeKind.Utc), + End: new DateTime(2020, 01, 02, 0, 0, 0, DateTimeKind.Utc)) + }; - Assert.True(expectedIntervals2.SequenceEqual(actualIntervals2)); + Assert.True(expectedIntervals2.SequenceEqual(actualIntervals2)); - stream.Seek(6 * sizeof(double), SeekOrigin.Begin); - Assert.Equal(66.6, reader.ReadDouble()); - Assert.Equal(77.7, reader.ReadDouble()); - } + stream.Seek(6 * sizeof(double), SeekOrigin.Begin); + Assert.Equal(66.6, reader.ReadDouble()); + Assert.Equal(77.7, reader.ReadDouble()); } } \ No newline at end of file diff --git a/tests/Nexus.Tests/Other/CatalogContainersExtensionsTests.cs b/tests/Nexus.Tests/Other/CatalogContainersExtensionsTests.cs index 0c56cacb..c230b240 100644 --- a/tests/Nexus.Tests/Other/CatalogContainersExtensionsTests.cs +++ b/tests/Nexus.Tests/Other/CatalogContainersExtensionsTests.cs @@ -5,163 +5,162 @@ using Nexus.Services; using Xunit; -namespace Other +namespace Other; + +public class CatalogContainersExtensionsTests { - public class CatalogContainersExtensionsTests + [Fact] + public async Task CanTryFindCatalogContainer() { - [Fact] - public async Task CanTryFindCatalogContainer() - { - // arrange - var catalogManager = Mock.Of(); - - Mock.Get(catalogManager) - .Setup(catalogManager => catalogManager.GetCatalogContainersAsync( - It.IsAny(), - It.IsAny())) - .Returns((container, token) => + // arrange + var catalogManager = Mock.Of(); + + Mock.Get(catalogManager) + .Setup(catalogManager => catalogManager.GetCatalogContainersAsync( + It.IsAny(), + It.IsAny())) + .Returns((container, token) => + { + return Task.FromResult(container.Id switch { - return Task.FromResult(container.Id switch + "/" => new CatalogContainer[] + { + new CatalogContainer(new CatalogRegistration("/A", string.Empty), default!, default!, default!, default!, catalogManager, default!, default!), + }, + "/A" => new CatalogContainer[] + { + new CatalogContainer(new CatalogRegistration("/A/C", string.Empty), default!, default!, default!, default!, catalogManager, default!, default!), + new CatalogContainer(new CatalogRegistration("/A/B", string.Empty), default!, default!, default!, default!, catalogManager, default!, default!), + new CatalogContainer(new CatalogRegistration("/A/D", string.Empty), default!, default!, default!, default!, catalogManager, default!, default!) + }, + "/A/B" => new CatalogContainer[] + { + new CatalogContainer(new CatalogRegistration("/A/B/D", string.Empty), default!, default!, default!, default!, catalogManager, default!, default!), + new CatalogContainer(new CatalogRegistration("/A/B/C", string.Empty), default!, default!, default!, default!, catalogManager, default!, default!) + }, + "/A/D" => new CatalogContainer[] + { + new CatalogContainer(new CatalogRegistration("/A/D/F", string.Empty), default!, default!, default!, default!, catalogManager, default!, default!), + new CatalogContainer(new CatalogRegistration("/A/D/E", string.Empty), default!, default!, default!, default!, catalogManager, default!, default!), + new CatalogContainer(new CatalogRegistration("/A/D/E2", string.Empty), default!, default!, default!, default!, catalogManager, default!, default!) + }, + "/A/F" => new CatalogContainer[] { - "/" => new CatalogContainer[] - { - new CatalogContainer(new CatalogRegistration("/A", string.Empty), default!, default!, default!, default!, catalogManager, default!, default!), - }, - "/A" => new CatalogContainer[] - { - new CatalogContainer(new CatalogRegistration("/A/C", string.Empty), default!, default!, default!, default!, catalogManager, default!, default!), - new CatalogContainer(new CatalogRegistration("/A/B", string.Empty), default!, default!, default!, default!, catalogManager, default!, default!), - new CatalogContainer(new CatalogRegistration("/A/D", string.Empty), default!, default!, default!, default!, catalogManager, default!, default!) - }, - "/A/B" => new CatalogContainer[] - { - new CatalogContainer(new CatalogRegistration("/A/B/D", string.Empty), default!, default!, default!, default!, catalogManager, default!, default!), - new CatalogContainer(new CatalogRegistration("/A/B/C", string.Empty), default!, default!, default!, default!, catalogManager, default!, default!) - }, - "/A/D" => new CatalogContainer[] - { - new CatalogContainer(new CatalogRegistration("/A/D/F", string.Empty), default!, default!, default!, default!, catalogManager, default!, default!), - new CatalogContainer(new CatalogRegistration("/A/D/E", string.Empty), default!, default!, default!, default!, catalogManager, default!, default!), - new CatalogContainer(new CatalogRegistration("/A/D/E2", string.Empty), default!, default!, default!, default!, catalogManager, default!, default!) - }, - "/A/F" => new CatalogContainer[] - { - new CatalogContainer(new CatalogRegistration("/A/F/H", string.Empty), default!, default!, default!, default!, catalogManager, default!, default!) - }, - _ => throw new Exception($"Unsupported combination {container.Id}.") - }); + new CatalogContainer(new CatalogRegistration("/A/F/H", string.Empty), default!, default!, default!, default!, catalogManager, default!, default!) + }, + _ => throw new Exception($"Unsupported combination {container.Id}.") }); + }); - var root = CatalogContainer.CreateRoot(catalogManager, default!); - - // act - var catalogContainerA = await root.TryFindCatalogContainerAsync("/A/B/C", CancellationToken.None); - var catalogContainerB = await root.TryFindCatalogContainerAsync("/A/D/E", CancellationToken.None); - var catalogContainerB2 = await root.TryFindCatalogContainerAsync("/A/D/E2", CancellationToken.None); - var catalogContainerC = await root.TryFindCatalogContainerAsync("/A/F/G", CancellationToken.None); - - // assert - Assert.NotNull(catalogContainerA); - Assert.Equal("/A/B/C", catalogContainerA?.Id); - - Assert.NotNull(catalogContainerB); - Assert.Equal("/A/D/E", catalogContainerB?.Id); - - Assert.NotNull(catalogContainerB2); - Assert.Equal("/A/D/E2", catalogContainerB2?.Id); + var root = CatalogContainer.CreateRoot(catalogManager, default!); - Assert.Null(catalogContainerC); - } + // act + var catalogContainerA = await root.TryFindCatalogContainerAsync("/A/B/C", CancellationToken.None); + var catalogContainerB = await root.TryFindCatalogContainerAsync("/A/D/E", CancellationToken.None); + var catalogContainerB2 = await root.TryFindCatalogContainerAsync("/A/D/E2", CancellationToken.None); + var catalogContainerC = await root.TryFindCatalogContainerAsync("/A/F/G", CancellationToken.None); - [Fact] - public async Task CanTryFind() - { - // arrange - var representation1 = new Representation(NexusDataType.FLOAT64, TimeSpan.FromMilliseconds(1)); - var representation2 = new Representation(NexusDataType.FLOAT64, TimeSpan.FromMilliseconds(100)); + // assert + Assert.NotNull(catalogContainerA); + Assert.Equal("/A/B/C", catalogContainerA?.Id); - var resource = new ResourceBuilder("T1") - .AddRepresentation(representation1) - .AddRepresentation(representation2) - .Build(); + Assert.NotNull(catalogContainerB); + Assert.Equal("/A/D/E", catalogContainerB?.Id); - var catalog = new ResourceCatalogBuilder("/A/B/C") - .AddResource(resource) - .Build(); + Assert.NotNull(catalogContainerB2); + Assert.Equal("/A/D/E2", catalogContainerB2?.Id); - var dataSourceController = Mock.Of(); - - Mock.Get(dataSourceController) - .Setup(dataSourceController => dataSourceController.GetCatalogAsync( - It.IsAny(), - It.IsAny())) - .ReturnsAsync(catalog); - - Mock.Get(dataSourceController) - .Setup(dataSourceController => dataSourceController.GetTimeRangeAsync( - It.IsAny(), - It.IsAny())) - .ReturnsAsync(new CatalogTimeRange(default, default)); - - var dataControllerService = Mock.Of(); - - Mock.Get(dataControllerService) - .Setup(dataControllerService => dataControllerService.GetDataSourceControllerAsync( - It.IsAny(), - It.IsAny())) - .ReturnsAsync(dataSourceController); - - var catalogManager = Mock.Of(); + Assert.Null(catalogContainerC); + } - Mock.Get(catalogManager) - .Setup(catalogManager => catalogManager.GetCatalogContainersAsync( - It.IsAny(), - It.IsAny())) - .Returns((container, token) => + [Fact] + public async Task CanTryFind() + { + // arrange + var representation1 = new Representation(NexusDataType.FLOAT64, TimeSpan.FromMilliseconds(1)); + var representation2 = new Representation(NexusDataType.FLOAT64, TimeSpan.FromMilliseconds(100)); + + var resource = new ResourceBuilder("T1") + .AddRepresentation(representation1) + .AddRepresentation(representation2) + .Build(); + + var catalog = new ResourceCatalogBuilder("/A/B/C") + .AddResource(resource) + .Build(); + + var dataSourceController = Mock.Of(); + + Mock.Get(dataSourceController) + .Setup(dataSourceController => dataSourceController.GetCatalogAsync( + It.IsAny(), + It.IsAny())) + .ReturnsAsync(catalog); + + Mock.Get(dataSourceController) + .Setup(dataSourceController => dataSourceController.GetTimeRangeAsync( + It.IsAny(), + It.IsAny())) + .ReturnsAsync(new CatalogTimeRange(default, default)); + + var dataControllerService = Mock.Of(); + + Mock.Get(dataControllerService) + .Setup(dataControllerService => dataControllerService.GetDataSourceControllerAsync( + It.IsAny(), + It.IsAny())) + .ReturnsAsync(dataSourceController); + + var catalogManager = Mock.Of(); + + Mock.Get(catalogManager) + .Setup(catalogManager => catalogManager.GetCatalogContainersAsync( + It.IsAny(), + It.IsAny())) + .Returns((container, token) => + { + return Task.FromResult(container.Id switch { - return Task.FromResult(container.Id switch + "/" => new CatalogContainer[] { - "/" => new CatalogContainer[] - { - new CatalogContainer(new CatalogRegistration("/A/B/C", string.Empty), default!, default!, default!, default!, default!, default!, dataControllerService), - }, - _ => throw new Exception("Unsupported combination.") - }); + new CatalogContainer(new CatalogRegistration("/A/B/C", string.Empty), default!, default!, default!, default!, default!, default!, dataControllerService), + }, + _ => throw new Exception("Unsupported combination.") }); - - var root = CatalogContainer.CreateRoot(catalogManager, default!); - - // act - var request1 = await root.TryFindAsync("/A/B/C/T1/1_ms", CancellationToken.None); - var request2 = await root.TryFindAsync("/A/B/C/T1/10_ms", CancellationToken.None); - var request3 = await root.TryFindAsync("/A/B/C/T1/100_ms", CancellationToken.None); - var request4 = await root.TryFindAsync("/A/B/C/T1/1_s_mean_polar_deg", CancellationToken.None); - var request5 = await root.TryFindAsync("/A/B/C/T1/1_s_min_bitwise#base=1_ms", CancellationToken.None); - var request6 = await root.TryFindAsync("/A/B/C/T1/1_s_max_bitwise#base=100_ms", CancellationToken.None); - - // assert - Assert.NotNull(request1); - Assert.Null(request2); - Assert.NotNull(request3); - Assert.NotNull(request4); - Assert.NotNull(request5); - Assert.NotNull(request6); - - Assert.Null(request1!.BaseItem); - Assert.Null(request3!.BaseItem); - Assert.NotNull(request4!.BaseItem); - Assert.NotNull(request5!.BaseItem); - Assert.NotNull(request6!.BaseItem); - - Assert.Equal("/A/B/C/T1/1_ms", request1.Item.ToPath()); - Assert.Equal("/A/B/C/T1/100_ms", request3.Item.ToPath()); - Assert.Equal("/A/B/C/T1/1_s_mean_polar_deg", request4.Item.ToPath()); - Assert.Equal("/A/B/C/T1/1_s_min_bitwise", request5.Item.ToPath()); - Assert.Equal("/A/B/C/T1/1_s_max_bitwise", request6.Item.ToPath()); - - Assert.Equal("/A/B/C/T1/1_ms", request4.BaseItem!.ToPath()); - Assert.Equal("/A/B/C/T1/1_ms", request5.BaseItem!.ToPath()); - Assert.Equal("/A/B/C/T1/100_ms", request6.BaseItem!.ToPath()); - } + }); + + var root = CatalogContainer.CreateRoot(catalogManager, default!); + + // act + var request1 = await root.TryFindAsync("/A/B/C/T1/1_ms", CancellationToken.None); + var request2 = await root.TryFindAsync("/A/B/C/T1/10_ms", CancellationToken.None); + var request3 = await root.TryFindAsync("/A/B/C/T1/100_ms", CancellationToken.None); + var request4 = await root.TryFindAsync("/A/B/C/T1/1_s_mean_polar_deg", CancellationToken.None); + var request5 = await root.TryFindAsync("/A/B/C/T1/1_s_min_bitwise#base=1_ms", CancellationToken.None); + var request6 = await root.TryFindAsync("/A/B/C/T1/1_s_max_bitwise#base=100_ms", CancellationToken.None); + + // assert + Assert.NotNull(request1); + Assert.Null(request2); + Assert.NotNull(request3); + Assert.NotNull(request4); + Assert.NotNull(request5); + Assert.NotNull(request6); + + Assert.Null(request1!.BaseItem); + Assert.Null(request3!.BaseItem); + Assert.NotNull(request4!.BaseItem); + Assert.NotNull(request5!.BaseItem); + Assert.NotNull(request6!.BaseItem); + + Assert.Equal("/A/B/C/T1/1_ms", request1.Item.ToPath()); + Assert.Equal("/A/B/C/T1/100_ms", request3.Item.ToPath()); + Assert.Equal("/A/B/C/T1/1_s_mean_polar_deg", request4.Item.ToPath()); + Assert.Equal("/A/B/C/T1/1_s_min_bitwise", request5.Item.ToPath()); + Assert.Equal("/A/B/C/T1/1_s_max_bitwise", request6.Item.ToPath()); + + Assert.Equal("/A/B/C/T1/1_ms", request4.BaseItem!.ToPath()); + Assert.Equal("/A/B/C/T1/1_ms", request5.BaseItem!.ToPath()); + Assert.Equal("/A/B/C/T1/100_ms", request6.BaseItem!.ToPath()); } } \ No newline at end of file diff --git a/tests/Nexus.Tests/Other/LoggingTests.cs b/tests/Nexus.Tests/Other/LoggingTests.cs index c54eb03a..21db804c 100644 --- a/tests/Nexus.Tests/Other/LoggingTests.cs +++ b/tests/Nexus.Tests/Other/LoggingTests.cs @@ -6,145 +6,144 @@ using System.Text.RegularExpressions; using Xunit; -namespace Other -{ - // Information from Microsoft: - // https://docs.microsoft.com/en-us/aspnet/core/fundamentals/logging/?view=aspnetcore-2.2#log-level +namespace Other; + +// Information from Microsoft: +// https://docs.microsoft.com/en-us/aspnet/core/fundamentals/logging/?view=aspnetcore-2.2#log-level - // Best practices: - // https://blog.rsuter.com/logging-with-ilogger-recommendations-and-best-practices/ +// Best practices: +// https://blog.rsuter.com/logging-with-ilogger-recommendations-and-best-practices/ - // Attaching a large state might lead to very large logs. Nicholas Blumhardt recommends to - // simply send a single verbose message with identifier and all other messages should contain - // that identifier, too, so they can be correlated later ("log once, correlate later"). +// Attaching a large state might lead to very large logs. Nicholas Blumhardt recommends to +// simply send a single verbose message with identifier and all other messages should contain +// that identifier, too, so they can be correlated later ("log once, correlate later"). - public class LoggingTests +public class LoggingTests +{ + [Fact] + public void CanSerilog() { - [Fact] - public void CanSerilog() + // create dirs + var root = Path.Combine(Path.GetTempPath(), $"Nexus.Tests.{Guid.NewGuid()}"); + Directory.CreateDirectory(root); + + // 1. Configure Serilog + Environment.SetEnvironmentVariable("NEXUS_SERILOG__MINIMUMLEVEL__OVERRIDE__Nexus.Services", "Verbose"); + + Environment.SetEnvironmentVariable("NEXUS_SERILOG__WRITETO__1__NAME", "File"); + Environment.SetEnvironmentVariable("NEXUS_SERILOG__WRITETO__1__ARGS__PATH", Path.Combine(root, "log.txt")); + Environment.SetEnvironmentVariable("NEXUS_SERILOG__WRITETO__1__ARGS__OUTPUTTEMPLATE", "[{Level:u3}] {MyCustomProperty} {Message}{NewLine}{Exception}"); + + Environment.SetEnvironmentVariable("NEXUS_SERILOG__WRITETO__2__NAME", "GrafanaLoki"); + Environment.SetEnvironmentVariable("NEXUS_SERILOG__WRITETO__2__ARGS__URI", "http://localhost:3100"); + Environment.SetEnvironmentVariable("NEXUS_SERILOG__WRITETO__2__ARGS__LABELS__0__KEY", "app"); + Environment.SetEnvironmentVariable("NEXUS_SERILOG__WRITETO__2__ARGS__LABELS__0__VALUE", "nexus"); + Environment.SetEnvironmentVariable("NEXUS_SERILOG__WRITETO__2__ARGS__OUTPUTTEMPLATE", "{Message}{NewLine}{Exception}"); + + Environment.SetEnvironmentVariable("NEXUS_SERILOG__WRITETO__3__NAME", "Seq"); + Environment.SetEnvironmentVariable("NEXUS_SERILOG__WRITETO__3__ARGS__SERVERURL", "http://localhost:5341"); + + Environment.SetEnvironmentVariable("NEXUS_SERILOG__ENRICH__1", "WithMachineName"); + + // 2. Build the configuration + var configuration = NexusOptionsBase.BuildConfiguration(Array.Empty()); + + var serilogger = new LoggerConfiguration() + .ReadFrom.Configuration(configuration) + .Enrich.WithProperty("MyCustomProperty", "MyCustomValue") + .CreateLogger(); + + var loggerFactory = new SerilogLoggerFactory(serilogger); + + // 3. Create a logger + var logger = loggerFactory.CreateLogger(); + + // 3.1 Log-levels + logger.LogTrace("Trace"); + logger.LogDebug("Debug"); + logger.LogInformation("Information"); + logger.LogWarning("Warning"); + logger.LogError("Error"); + logger.LogCritical("Critical"); + + // 3.2 Log with exception + try + { + throw new Exception("Something went wrong?!"); + } + catch (Exception ex) + { + logger.LogError(ex, "Error"); + } + + // 3.3 Log with template + var context1 = new { Amount = 108, Message = "Hello" }; + var context2 = new { Amount2 = 108, Message2 = "Hello" }; + + logger.LogInformation("Log with template with parameters {Text}, {Number} and {AnonymousType}", "A", 2.59, context1); + + // 3.4 Log with scope with template + using (var scopeWithTemplate = logger.BeginScope("My templated scope message with parameters {ScopeText}, {ScopeNumber} and {ScopeAnonymousType}", "A", 2.59, context1)) + { + logger.LogInformation("Log with scope with template with parameters {Text}, {Number} and {AnonymousType}", "A", 2.59, context1); + } + + // 3.5 Log with double scope with template + using (var scopeWithTemplate1 = logger.BeginScope("My templated scope message 1 with parameters {ScopeText1}, {ScopeNumber} and {ScopeAnonymousType}", "A", 2.59, context1)) + { + using var scopeWithTemplate2 = logger.BeginScope("My templated scope message 2 with parameters {ScopeText2}, {ScopeNumber} and {ScopeAnonymousType}", "A", 3.59, context2); + logger.LogInformation("Log with double scope with template with parameters {Text}, {Number} and {AnonymousType}", "A", 2.59, context1); + } + + // 3.6 Log with scope with state + using (var scopeWithState = logger.BeginScope(context1)) + { + logger.LogInformation("Log with scope with state with parameters {Text}, {Number} and {AnonymousType}", "A", 2.59, context1); + } + + // 3.7 Log with double scope with state + using (var scopeWithState1 = logger.BeginScope(context1)) + { + using var scopeWithState2 = logger.BeginScope(context2); + logger.LogInformation("Log with double scope with state with parameters {Text}, {Number} and {AnonymousType}", "A", 2.59, context1); + } + + // 3.8 Log with scope with Dictionary state + using (var scopeWithState1 = logger.BeginScope(new Dictionary() + { + ["Amount"] = context1.Amount, + ["Message"] = context1.Message + })) + { + logger.LogInformation("Log with scope with Dictionary state"); + } + + // 3.9 Log with named scope + using (var namedScope = logger.BeginNamedScope("MyScopeName", new Dictionary() + { + ["Amount"] = context1.Amount, + ["Message"] = context1.Message + })) + { + logger.LogInformation("Log with named scope"); + } + + serilogger.Dispose(); + + // assert + var expected = Regex.Replace(Regex.Replace(File.ReadAllText("expected-log.txt"), "\r\n", "\n"), @"in .*?tests.*?LoggingTests.cs", ""); + var actual = Regex.Replace(Regex.Replace(File.ReadAllText(Path.Combine(root, "log.txt")), "\r\n", "\n"), @"in .*?tests.*?LoggingTests.cs", ""); + + Assert.Equal(expected, actual); + + // clean up + try + { + Directory.Delete(root, true); + } + catch { - // create dirs - var root = Path.Combine(Path.GetTempPath(), $"Nexus.Tests.{Guid.NewGuid()}"); - Directory.CreateDirectory(root); - - // 1. Configure Serilog - Environment.SetEnvironmentVariable("NEXUS_SERILOG__MINIMUMLEVEL__OVERRIDE__Nexus.Services", "Verbose"); - - Environment.SetEnvironmentVariable("NEXUS_SERILOG__WRITETO__1__NAME", "File"); - Environment.SetEnvironmentVariable("NEXUS_SERILOG__WRITETO__1__ARGS__PATH", Path.Combine(root, "log.txt")); - Environment.SetEnvironmentVariable("NEXUS_SERILOG__WRITETO__1__ARGS__OUTPUTTEMPLATE", "[{Level:u3}] {MyCustomProperty} {Message}{NewLine}{Exception}"); - - Environment.SetEnvironmentVariable("NEXUS_SERILOG__WRITETO__2__NAME", "GrafanaLoki"); - Environment.SetEnvironmentVariable("NEXUS_SERILOG__WRITETO__2__ARGS__URI", "http://localhost:3100"); - Environment.SetEnvironmentVariable("NEXUS_SERILOG__WRITETO__2__ARGS__LABELS__0__KEY", "app"); - Environment.SetEnvironmentVariable("NEXUS_SERILOG__WRITETO__2__ARGS__LABELS__0__VALUE", "nexus"); - Environment.SetEnvironmentVariable("NEXUS_SERILOG__WRITETO__2__ARGS__OUTPUTTEMPLATE", "{Message}{NewLine}{Exception}"); - - Environment.SetEnvironmentVariable("NEXUS_SERILOG__WRITETO__3__NAME", "Seq"); - Environment.SetEnvironmentVariable("NEXUS_SERILOG__WRITETO__3__ARGS__SERVERURL", "http://localhost:5341"); - - Environment.SetEnvironmentVariable("NEXUS_SERILOG__ENRICH__1", "WithMachineName"); - - // 2. Build the configuration - var configuration = NexusOptionsBase.BuildConfiguration(Array.Empty()); - - var serilogger = new LoggerConfiguration() - .ReadFrom.Configuration(configuration) - .Enrich.WithProperty("MyCustomProperty", "MyCustomValue") - .CreateLogger(); - - var loggerFactory = new SerilogLoggerFactory(serilogger); - - // 3. Create a logger - var logger = loggerFactory.CreateLogger(); - - // 3.1 Log-levels - logger.LogTrace("Trace"); - logger.LogDebug("Debug"); - logger.LogInformation("Information"); - logger.LogWarning("Warning"); - logger.LogError("Error"); - logger.LogCritical("Critical"); - - // 3.2 Log with exception - try - { - throw new Exception("Something went wrong?!"); - } - catch (Exception ex) - { - logger.LogError(ex, "Error"); - } - - // 3.3 Log with template - var context1 = new { Amount = 108, Message = "Hello" }; - var context2 = new { Amount2 = 108, Message2 = "Hello" }; - - logger.LogInformation("Log with template with parameters {Text}, {Number} and {AnonymousType}", "A", 2.59, context1); - - // 3.4 Log with scope with template - using (var scopeWithTemplate = logger.BeginScope("My templated scope message with parameters {ScopeText}, {ScopeNumber} and {ScopeAnonymousType}", "A", 2.59, context1)) - { - logger.LogInformation("Log with scope with template with parameters {Text}, {Number} and {AnonymousType}", "A", 2.59, context1); - } - - // 3.5 Log with double scope with template - using (var scopeWithTemplate1 = logger.BeginScope("My templated scope message 1 with parameters {ScopeText1}, {ScopeNumber} and {ScopeAnonymousType}", "A", 2.59, context1)) - { - using var scopeWithTemplate2 = logger.BeginScope("My templated scope message 2 with parameters {ScopeText2}, {ScopeNumber} and {ScopeAnonymousType}", "A", 3.59, context2); - logger.LogInformation("Log with double scope with template with parameters {Text}, {Number} and {AnonymousType}", "A", 2.59, context1); - } - - // 3.6 Log with scope with state - using (var scopeWithState = logger.BeginScope(context1)) - { - logger.LogInformation("Log with scope with state with parameters {Text}, {Number} and {AnonymousType}", "A", 2.59, context1); - } - - // 3.7 Log with double scope with state - using (var scopeWithState1 = logger.BeginScope(context1)) - { - using var scopeWithState2 = logger.BeginScope(context2); - logger.LogInformation("Log with double scope with state with parameters {Text}, {Number} and {AnonymousType}", "A", 2.59, context1); - } - - // 3.8 Log with scope with Dictionary state - using (var scopeWithState1 = logger.BeginScope(new Dictionary() - { - ["Amount"] = context1.Amount, - ["Message"] = context1.Message - })) - { - logger.LogInformation("Log with scope with Dictionary state"); - } - - // 3.9 Log with named scope - using (var namedScope = logger.BeginNamedScope("MyScopeName", new Dictionary() - { - ["Amount"] = context1.Amount, - ["Message"] = context1.Message - })) - { - logger.LogInformation("Log with named scope"); - } - - serilogger.Dispose(); - - // assert - var expected = Regex.Replace(Regex.Replace(File.ReadAllText("expected-log.txt"), "\r\n", "\n"), @"in .*?tests.*?LoggingTests.cs", ""); - var actual = Regex.Replace(Regex.Replace(File.ReadAllText(Path.Combine(root, "log.txt")), "\r\n", "\n"), @"in .*?tests.*?LoggingTests.cs", ""); - - Assert.Equal(expected, actual); - - // clean up - try - { - Directory.Delete(root, true); - } - catch - { - // - } + // } } } \ No newline at end of file diff --git a/tests/Nexus.Tests/Other/OptionsTests.cs b/tests/Nexus.Tests/Other/OptionsTests.cs index 7dd4e582..d5094696 100644 --- a/tests/Nexus.Tests/Other/OptionsTests.cs +++ b/tests/Nexus.Tests/Other/OptionsTests.cs @@ -2,32 +2,49 @@ using Nexus.Core; using Xunit; -namespace Other +namespace Other; + +public class OptionsTests { - public class OptionsTests + private static readonly object _lock = new(); + + [InlineData(GeneralOptions.Section, typeof(GeneralOptions))] + [InlineData(DataOptions.Section, typeof(DataOptions))] + [InlineData(PathsOptions.Section, typeof(PathsOptions))] + [InlineData(SecurityOptions.Section, typeof(SecurityOptions))] + [Theory] + public void CanBindOptions(string section, Type optionsType) { - private static readonly object _lock = new(); - - [InlineData(GeneralOptions.Section, typeof(GeneralOptions))] - [InlineData(DataOptions.Section, typeof(DataOptions))] - [InlineData(PathsOptions.Section, typeof(PathsOptions))] - [InlineData(SecurityOptions.Section, typeof(SecurityOptions))] - [Theory] - public void CanBindOptions(string section, Type optionsType) - { - var configuration = NexusOptionsBase - .BuildConfiguration(Array.Empty()); + var configuration = NexusOptionsBase + .BuildConfiguration(Array.Empty()); - var options = (NexusOptionsBase)configuration - .GetSection(section) - .Get(optionsType)!; + var options = (NexusOptionsBase)configuration + .GetSection(section) + .Get(optionsType)!; - Assert.Equal(section, options.BlindSample); - } + Assert.Equal(section, options.BlindSample); + } + + [Fact] + public void CanReadAppsettingsJson() + { + var configuration = NexusOptionsBase + .BuildConfiguration(Array.Empty()); + + var options = configuration + .GetSection(DataOptions.Section) + .Get()!; - [Fact] - public void CanReadAppsettingsJson() + Assert.Equal(0.99, options.AggregationNaNThreshold); + } + + [Fact] + public void CanOverrideAppsettingsJson_With_Json() + { + lock (_lock) { + Environment.SetEnvironmentVariable("NEXUS_PATHS__SETTINGS", "myappsettings.json"); + var configuration = NexusOptionsBase .BuildConfiguration(Array.Empty()); @@ -35,86 +52,68 @@ public void CanReadAppsettingsJson() .GetSection(DataOptions.Section) .Get()!; - Assert.Equal(0.99, options.AggregationNaNThreshold); - } - - [Fact] - public void CanOverrideAppsettingsJson_With_Json() - { - lock (_lock) - { - Environment.SetEnvironmentVariable("NEXUS_PATHS__SETTINGS", "myappsettings.json"); + Environment.SetEnvironmentVariable("NEXUS_PATHS__SETTINGS", null); - var configuration = NexusOptionsBase - .BuildConfiguration(Array.Empty()); - - var options = configuration - .GetSection(DataOptions.Section) - .Get()!; - - Environment.SetEnvironmentVariable("NEXUS_PATHS__SETTINGS", null); - - Assert.Equal(0.90, options.AggregationNaNThreshold); - } + Assert.Equal(0.90, options.AggregationNaNThreshold); } + } - [Fact] - public void CanOverrideIni_With_EnvironmentVariable() + [Fact] + public void CanOverrideIni_With_EnvironmentVariable() + { + lock (_lock) { - lock (_lock) - { - Environment.SetEnvironmentVariable("NEXUS_PATHS__SETTINGS", "myappsettings.ini"); + Environment.SetEnvironmentVariable("NEXUS_PATHS__SETTINGS", "myappsettings.ini"); - var configuration1 = NexusOptionsBase - .BuildConfiguration(Array.Empty()); + var configuration1 = NexusOptionsBase + .BuildConfiguration(Array.Empty()); - var options1 = configuration1 - .GetSection(DataOptions.Section) - .Get()!; + var options1 = configuration1 + .GetSection(DataOptions.Section) + .Get()!; - Environment.SetEnvironmentVariable("NEXUS_DATA__AGGREGATIONNANTHRESHOLD", "0.90"); + Environment.SetEnvironmentVariable("NEXUS_DATA__AGGREGATIONNANTHRESHOLD", "0.90"); - var configuration2 = NexusOptionsBase - .BuildConfiguration(Array.Empty()); + var configuration2 = NexusOptionsBase + .BuildConfiguration(Array.Empty()); - var options2 = configuration2 - .GetSection(DataOptions.Section) - .Get()!; + var options2 = configuration2 + .GetSection(DataOptions.Section) + .Get()!; - Environment.SetEnvironmentVariable("NEXUS_PATHS__SETTINGS", null); - Environment.SetEnvironmentVariable("NEXUS_DATA__AGGREGATIONNANTHRESHOLD", null); + Environment.SetEnvironmentVariable("NEXUS_PATHS__SETTINGS", null); + Environment.SetEnvironmentVariable("NEXUS_DATA__AGGREGATIONNANTHRESHOLD", null); - Assert.Equal(0.80, options1.AggregationNaNThreshold); - Assert.Equal(0.90, options2.AggregationNaNThreshold); - } + Assert.Equal(0.80, options1.AggregationNaNThreshold); + Assert.Equal(0.90, options2.AggregationNaNThreshold); } + } - [InlineData("DATA:AGGREGATIONNANTHRESHOLD=0.99")] - [InlineData("/DATA:AGGREGATIONNANTHRESHOLD=0.99")] - [InlineData("--DATA:AGGREGATIONNANTHRESHOLD=0.99")] + [InlineData("DATA:AGGREGATIONNANTHRESHOLD=0.99")] + [InlineData("/DATA:AGGREGATIONNANTHRESHOLD=0.99")] + [InlineData("--DATA:AGGREGATIONNANTHRESHOLD=0.99")] - [InlineData("data:aggregationnanthreshold=0.99")] - [InlineData("/data:aggregationnanthreshold=0.99")] - [InlineData("--data:aggregationnanthreshold=0.99")] + [InlineData("data:aggregationnanthreshold=0.99")] + [InlineData("/data:aggregationnanthreshold=0.99")] + [InlineData("--data:aggregationnanthreshold=0.99")] - [Theory] - public void CanOverrideEnvironmentVariable_With_CommandLineParameter(string arg) + [Theory] + public void CanOverrideEnvironmentVariable_With_CommandLineParameter(string arg) + { + lock (_lock) { - lock (_lock) - { - Environment.SetEnvironmentVariable("NEXUS_DATA__AGGREGATIONNANTHRESHOLD", "0.90"); + Environment.SetEnvironmentVariable("NEXUS_DATA__AGGREGATIONNANTHRESHOLD", "0.90"); - var configuration = NexusOptionsBase - .BuildConfiguration(new string[] { arg }); + var configuration = NexusOptionsBase + .BuildConfiguration(new string[] { arg }); - var options = configuration - .GetSection(DataOptions.Section) - .Get()!; + var options = configuration + .GetSection(DataOptions.Section) + .Get()!; - Environment.SetEnvironmentVariable("NEXUS_DATA__AGGREGATIONNANTHRESHOLD", null); + Environment.SetEnvironmentVariable("NEXUS_DATA__AGGREGATIONNANTHRESHOLD", null); - Assert.Equal(0.99, options.AggregationNaNThreshold); - } + Assert.Equal(0.99, options.AggregationNaNThreshold); } } } \ No newline at end of file diff --git a/tests/Nexus.Tests/Other/UtilitiesTests.cs b/tests/Nexus.Tests/Other/UtilitiesTests.cs index f25b2347..45934997 100644 --- a/tests/Nexus.Tests/Other/UtilitiesTests.cs +++ b/tests/Nexus.Tests/Other/UtilitiesTests.cs @@ -6,275 +6,274 @@ using Xunit; using static OpenIddict.Abstractions.OpenIddictConstants; -namespace Other +namespace Other; + +public class UtilitiesTests { - public class UtilitiesTests + [Theory] + + [InlineData("Basic", true, new string[0], new string[0], new string[0], true)] + [InlineData("Basic", false, new string[] { "/D/E/F", "/A/B/C", "/G/H/I" }, new string[0], new string[0], true)] + [InlineData("Basic", false, new string[] { "^/A/B/.*" }, new string[0], new string[0], true)] + [InlineData("Basic", false, new string[0], new string[] { "A" }, new string[0], true)] + + [InlineData("Basic", false, new string[0], new string[0], new string[0], false)] + [InlineData("Basic", false, new string[] { "/D/E/F", "/A/B/C2", "/G/H/I" }, new string[0], new string[0], false)] + [InlineData("Basic", false, new string[0], new string[] { "A2" }, new string[0], false)] + [InlineData(null, true, new string[0], new string[0], new string[0], false)] + + [InlineData(PersonalAccessTokenAuthenticationDefaults.AuthenticationScheme, true, new string[0], new string[0], new string[0], true)] + [InlineData(PersonalAccessTokenAuthenticationDefaults.AuthenticationScheme, false, new string[] { "/A/B/C" }, new string[0], new string[] { "/A/B/" }, true)] + [InlineData(PersonalAccessTokenAuthenticationDefaults.AuthenticationScheme, false, new string[] { "/A/B/C" }, new string[0], new string[] { "/D/E/" }, false)] + public void CanDetermineCatalogReadability( + string? authenticationType, + bool isAdmin, + string[] canReadCatalog, + string[] canReadCatalogGroup, + string[] patUserCanReadCatalog, + bool expected) + { + // Arrange + var isPAT = authenticationType == PersonalAccessTokenAuthenticationDefaults.AuthenticationScheme; + + var roleClaimType = isPAT + ? NexusClaims.ToPatUserClaimType(Claims.Role) + : Claims.Role; + + var catalogId = "/A/B/C"; + var catalogMetadata = new CatalogMetadata(default, GroupMemberships: ["A"], default); + + var adminClaim = isAdmin + ? [new Claim(roleClaimType, NexusRoles.ADMINISTRATOR)] + : Array.Empty(); + + var principal = new ClaimsPrincipal( + new ClaimsIdentity( + claims: adminClaim + .Concat(canReadCatalog.Select(value => new Claim(NexusClaims.CAN_READ_CATALOG, value))) + .Concat(canReadCatalogGroup.Select(value => new Claim(NexusClaims.CAN_READ_CATALOG_GROUP, value))) + .Concat(patUserCanReadCatalog.Select(value => new Claim(NexusClaims.ToPatUserClaimType(NexusClaims.CAN_READ_CATALOG), value))), + authenticationType, + nameType: Claims.Name, + roleType: Claims.Role)); + + // Act + var actual = AuthUtilities.IsCatalogReadable(catalogId, catalogMetadata, default!, principal); + + // Assert + Assert.Equal(expected, actual); + } + + [Theory] + + [InlineData("Basic", true, new string[0], new string[0], new string[0], true)] + [InlineData("Basic", false, new string[] { "/D/E/F", "/A/B/C", "/G/H/I" }, new string[0], new string[0], true)] + [InlineData("Basic", false, new string[] { "^/A/B/.*" }, new string[0], new string[0], true)] + [InlineData("Basic", false, new string[0], new string[] { "A" }, new string[0], true)] + + [InlineData("Basic", false, new string[0], new string[0], new string[0], false)] + [InlineData("Basic", false, new string[] { "/D/E/F", "/A/B/C2", "/G/H/I" }, new string[0], new string[0], false)] + [InlineData("Basic", false, new string[0], new string[] { "A2" }, new string[0], false)] + [InlineData(null, true, new string[0], new string[0], new string[0], false)] + + [InlineData(PersonalAccessTokenAuthenticationDefaults.AuthenticationScheme, true, new string[0], new string[0], new string[0], true)] + [InlineData(PersonalAccessTokenAuthenticationDefaults.AuthenticationScheme, false, new string[] { "/A/B/C" }, new string[0], new string[] { "/A/B/" }, true)] + [InlineData(PersonalAccessTokenAuthenticationDefaults.AuthenticationScheme, false, new string[] { "/A/B/C" }, new string[0], new string[] { "/D/E/" }, false)] + public void CanDetermineCatalogWritability( + string? authenticationType, + bool isAdmin, + string[] canWriteCatalog, + string[] canWriteCatalogGroup, + string[] patUserCanWriteCatalog, + bool expected) + { + // Arrange + var isPAT = authenticationType == PersonalAccessTokenAuthenticationDefaults.AuthenticationScheme; + + var roleClaimType = isPAT + ? NexusClaims.ToPatUserClaimType(Claims.Role) + : Claims.Role; + + var catalogId = "/A/B/C"; + var catalogMetadata = new CatalogMetadata(default, GroupMemberships: ["A"], default); + + var adminClaim = isAdmin + ? [new Claim(roleClaimType, NexusRoles.ADMINISTRATOR)] + : Array.Empty(); + + var principal = new ClaimsPrincipal( + new ClaimsIdentity( + claims: adminClaim + .Concat(canWriteCatalog.Select(value => new Claim(NexusClaims.CAN_WRITE_CATALOG, value))) + .Concat(canWriteCatalogGroup.Select(value => new Claim(NexusClaims.CAN_WRITE_CATALOG_GROUP, value))) + .Concat(patUserCanWriteCatalog.Select(value => new Claim(NexusClaims.ToPatUserClaimType(NexusClaims.CAN_WRITE_CATALOG), value))), + authenticationType, + nameType: Claims.Name, + roleType: Claims.Role)); + + // Act + var actual = AuthUtilities.IsCatalogWritable(catalogId, catalogMetadata, principal); + + // Assert + Assert.Equal(expected, actual); + } + + [Fact] + public void CanApplyRepresentationStatus() + { + // Arrange + var data = new int[] { 1, 2, 3, 4, 5, 6, 7, 8 }; + var status = new byte[] { 1, 0, 1, 0, 1, 0, 1, 0 }; + var actual = new double[status.Length]; + var expected = new double[] { 1, double.NaN, 3, double.NaN, 5, double.NaN, 7, double.NaN }; + + // Act + BufferUtilities.ApplyRepresentationStatus(data, status, actual); + + // Assert + Assert.True(expected.SequenceEqual(actual.ToArray())); + } + + [Fact] + public void CanApplyRepresentationStatusByType() + { + // Arrange + var data = new CastMemoryManager(new int[] { 1, 2, 3, 4, 5, 6, 7, 8 }).Memory; + var status = new byte[] { 1, 0, 1, 0, 1, 0, 1, 0 }; + var actual = new double[status.Length]; + var expected = new double[] { 1, double.NaN, 3, double.NaN, 5, double.NaN, 7, double.NaN }; + + // Act + BufferUtilities.ApplyRepresentationStatusByDataType(NexusDataType.INT32, data, status, actual); + + // Assert + Assert.True(expected.SequenceEqual(actual.ToArray())); + } + + public static IList ToDoubleData { get; } = new List { - [Theory] - - [InlineData("Basic", true, new string[0], new string[0], new string[0], true)] - [InlineData("Basic", false, new string[] { "/D/E/F", "/A/B/C", "/G/H/I" }, new string[0], new string[0], true)] - [InlineData("Basic", false, new string[] { "^/A/B/.*" }, new string[0], new string[0], true)] - [InlineData("Basic", false, new string[0], new string[] { "A" }, new string[0], true)] - - [InlineData("Basic", false, new string[0], new string[0], new string[0], false)] - [InlineData("Basic", false, new string[] { "/D/E/F", "/A/B/C2", "/G/H/I" }, new string[0], new string[0], false)] - [InlineData("Basic", false, new string[0], new string[] { "A2" }, new string[0], false)] - [InlineData(null, true, new string[0], new string[0], new string[0], false)] - - [InlineData(PersonalAccessTokenAuthenticationDefaults.AuthenticationScheme, true, new string[0], new string[0], new string[0], true)] - [InlineData(PersonalAccessTokenAuthenticationDefaults.AuthenticationScheme, false, new string[] { "/A/B/C" }, new string[0], new string[] { "/A/B/" }, true)] - [InlineData(PersonalAccessTokenAuthenticationDefaults.AuthenticationScheme, false, new string[] { "/A/B/C" }, new string[0], new string[] { "/D/E/" }, false)] - public void CanDetermineCatalogReadability( - string? authenticationType, - bool isAdmin, - string[] canReadCatalog, - string[] canReadCatalogGroup, - string[] patUserCanReadCatalog, - bool expected) - { - // Arrange - var isPAT = authenticationType == PersonalAccessTokenAuthenticationDefaults.AuthenticationScheme; - - var roleClaimType = isPAT - ? NexusClaims.ToPatUserClaimType(Claims.Role) - : Claims.Role; - - var catalogId = "/A/B/C"; - var catalogMetadata = new CatalogMetadata(default, GroupMemberships: ["A"], default); - - var adminClaim = isAdmin - ? [new Claim(roleClaimType, NexusRoles.ADMINISTRATOR)] - : Array.Empty(); - - var principal = new ClaimsPrincipal( - new ClaimsIdentity( - claims: adminClaim - .Concat(canReadCatalog.Select(value => new Claim(NexusClaims.CAN_READ_CATALOG, value))) - .Concat(canReadCatalogGroup.Select(value => new Claim(NexusClaims.CAN_READ_CATALOG_GROUP, value))) - .Concat(patUserCanReadCatalog.Select(value => new Claim(NexusClaims.ToPatUserClaimType(NexusClaims.CAN_READ_CATALOG), value))), - authenticationType, - nameType: Claims.Name, - roleType: Claims.Role)); - - // Act - var actual = AuthUtilities.IsCatalogReadable(catalogId, catalogMetadata, default!, principal); - - // Assert - Assert.Equal(expected, actual); - } - - [Theory] - - [InlineData("Basic", true, new string[0], new string[0], new string[0], true)] - [InlineData("Basic", false, new string[] { "/D/E/F", "/A/B/C", "/G/H/I" }, new string[0], new string[0], true)] - [InlineData("Basic", false, new string[] { "^/A/B/.*" }, new string[0], new string[0], true)] - [InlineData("Basic", false, new string[0], new string[] { "A" }, new string[0], true)] - - [InlineData("Basic", false, new string[0], new string[0], new string[0], false)] - [InlineData("Basic", false, new string[] { "/D/E/F", "/A/B/C2", "/G/H/I" }, new string[0], new string[0], false)] - [InlineData("Basic", false, new string[0], new string[] { "A2" }, new string[0], false)] - [InlineData(null, true, new string[0], new string[0], new string[0], false)] - - [InlineData(PersonalAccessTokenAuthenticationDefaults.AuthenticationScheme, true, new string[0], new string[0], new string[0], true)] - [InlineData(PersonalAccessTokenAuthenticationDefaults.AuthenticationScheme, false, new string[] { "/A/B/C" }, new string[0], new string[] { "/A/B/" }, true)] - [InlineData(PersonalAccessTokenAuthenticationDefaults.AuthenticationScheme, false, new string[] { "/A/B/C" }, new string[0], new string[] { "/D/E/" }, false)] - public void CanDetermineCatalogWritability( - string? authenticationType, - bool isAdmin, - string[] canWriteCatalog, - string[] canWriteCatalogGroup, - string[] patUserCanWriteCatalog, - bool expected) - { - // Arrange - var isPAT = authenticationType == PersonalAccessTokenAuthenticationDefaults.AuthenticationScheme; - - var roleClaimType = isPAT - ? NexusClaims.ToPatUserClaimType(Claims.Role) - : Claims.Role; - - var catalogId = "/A/B/C"; - var catalogMetadata = new CatalogMetadata(default, GroupMemberships: ["A"], default); - - var adminClaim = isAdmin - ? [new Claim(roleClaimType, NexusRoles.ADMINISTRATOR)] - : Array.Empty(); - - var principal = new ClaimsPrincipal( - new ClaimsIdentity( - claims: adminClaim - .Concat(canWriteCatalog.Select(value => new Claim(NexusClaims.CAN_WRITE_CATALOG, value))) - .Concat(canWriteCatalogGroup.Select(value => new Claim(NexusClaims.CAN_WRITE_CATALOG_GROUP, value))) - .Concat(patUserCanWriteCatalog.Select(value => new Claim(NexusClaims.ToPatUserClaimType(NexusClaims.CAN_WRITE_CATALOG), value))), - authenticationType, - nameType: Claims.Name, - roleType: Claims.Role)); - - // Act - var actual = AuthUtilities.IsCatalogWritable(catalogId, catalogMetadata, principal); - - // Assert - Assert.Equal(expected, actual); - } - - [Fact] - public void CanApplyRepresentationStatus() - { - // Arrange - var data = new int[] { 1, 2, 3, 4, 5, 6, 7, 8 }; - var status = new byte[] { 1, 0, 1, 0, 1, 0, 1, 0 }; - var actual = new double[status.Length]; - var expected = new double[] { 1, double.NaN, 3, double.NaN, 5, double.NaN, 7, double.NaN }; - - // Act - BufferUtilities.ApplyRepresentationStatus(data, status, actual); - - // Assert - Assert.True(expected.SequenceEqual(actual.ToArray())); - } - - [Fact] - public void CanApplyRepresentationStatusByType() - { - // Arrange - var data = new CastMemoryManager(new int[] { 1, 2, 3, 4, 5, 6, 7, 8 }).Memory; - var status = new byte[] { 1, 0, 1, 0, 1, 0, 1, 0 }; - var actual = new double[status.Length]; - var expected = new double[] { 1, double.NaN, 3, double.NaN, 5, double.NaN, 7, double.NaN }; - - // Act - BufferUtilities.ApplyRepresentationStatusByDataType(NexusDataType.INT32, data, status, actual); - - // Assert - Assert.True(expected.SequenceEqual(actual.ToArray())); - } - - public static IList ToDoubleData { get; } = new List - { - new object[]{ (byte)99, (double)99 }, - new object[]{ (sbyte)-99, (double)-99 }, - new object[]{ (ushort)99, (double)99 }, - new object[]{ (short)-99, (double)-99 }, - new object[]{ (uint)99, (double)99 }, - new object[]{ (int)-99, (double)-99 }, - new object[]{ (ulong)99, (double)99 }, - new object[]{ (long)-99, (double)-99 }, - new object[]{ (float)-99.123, (double)-99.123 }, - new object[]{ (double)-99.123, (double)-99.123 }, - }; - - [Theory] - [MemberData(nameof(ToDoubleData))] - public void CanGenericConvertToDouble(T value, double expected) - where T : unmanaged //, IEqualityComparer (does not compile correctly) - { - // Arrange - - // Act - var actual = GenericToDouble.ToDouble(value); - - // Assert - Assert.Equal(expected, actual, precision: 3); - } - - public static IList BitOrData { get; } = new List - { - new object[]{ (byte)3, (byte)4, (byte)7 }, - new object[]{ (sbyte)-2, (sbyte)-3, (sbyte)-1 }, - new object[]{ (ushort)3, (ushort)4, (ushort)7 }, - new object[]{ (short)-2, (short)-3, (short)-1 }, - new object[]{ (uint)3, (uint)4, (uint)7 }, - new object[]{ (int)-2, (int)-3, (int)-1 }, - new object[]{ (ulong)3, (ulong)4, (ulong)7 }, - new object[]{ (long)-2, (long)-3, (long)-1 }, - }; - - [Theory] - [MemberData(nameof(BitOrData))] - public void CanGenericBitOr(T a, T b, T expected) - where T : unmanaged //, IEqualityComparer (does not compile correctly) - { - // Arrange - - - // Act - var actual = GenericBitOr.BitOr(a, b); - - // Assert - Assert.Equal(expected, actual); - } - - public static IList BitAndData { get; } = new List - { - new object[]{ (byte)168, (byte)44, (byte)40 }, - new object[]{ (sbyte)-88, (sbyte)44, (sbyte)40 }, - new object[]{ (ushort)168, (ushort)44, (ushort)40 }, - new object[]{ (short)-88, (short)44, (short)40 }, - new object[]{ (uint)168, (uint)44, (uint)40 }, - new object[]{ (int)-88, (int)44, (int)40 }, - new object[]{ (ulong)168, (ulong)44, (ulong)40 }, - new object[]{ (long)-88, (long)44, (long)40 }, - }; - - [Theory] - [MemberData(nameof(BitAndData))] - public void CanGenericBitAnd(T a, T b, T expected) - where T : unmanaged //, IEqualityComparer (does not compile correctly) - { - // Arrange - - - // Act - var actual = GenericBitAnd.BitAnd(a, b); - - // Assert - Assert.Equal(expected, actual); - } - - record MyType(int A, string B, TimeSpan C); - - [Fact] - public void CanSerializeAndDeserializeTimeSpan() - { - // Arrange - var expected = new MyType(A: 1, B: "Two", C: TimeSpan.FromSeconds(1)); - - // Act - var jsonString = JsonSerializerHelper.SerializeIndented(expected); - var actual = JsonSerializer.Deserialize(jsonString); - - // Assert - Assert.Equal(expected, actual); - } - - [Fact] - public void CanCastMemory() - { - // Arrange - var values = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 }; - var expected = new int[] { 67305985, 134678021 }; - - // Act - var actual = new CastMemoryManager(values).Memory; - - // Assert - Assert.True(expected.SequenceEqual(actual.ToArray())); - } - - - [Fact] - public void CanDetermineSizeOfNexusDataType() - { - // Arrange - var values = NexusUtilities.GetEnumValues(); - var expected = new[] { 1, 2, 4, 8, 1, 2, 4, 8, 4, 8 }; - - // Act - var actual = values.Select(value => NexusUtilities.SizeOf(value)); - - // Assert - Assert.Equal(expected, actual); - } + new object[]{ (byte)99, (double)99 }, + new object[]{ (sbyte)-99, (double)-99 }, + new object[]{ (ushort)99, (double)99 }, + new object[]{ (short)-99, (double)-99 }, + new object[]{ (uint)99, (double)99 }, + new object[]{ (int)-99, (double)-99 }, + new object[]{ (ulong)99, (double)99 }, + new object[]{ (long)-99, (double)-99 }, + new object[]{ (float)-99.123, (double)-99.123 }, + new object[]{ (double)-99.123, (double)-99.123 }, + }; + + [Theory] + [MemberData(nameof(ToDoubleData))] + public void CanGenericConvertToDouble(T value, double expected) + where T : unmanaged //, IEqualityComparer (does not compile correctly) + { + // Arrange + + // Act + var actual = GenericToDouble.ToDouble(value); + + // Assert + Assert.Equal(expected, actual, precision: 3); + } + + public static IList BitOrData { get; } = new List + { + new object[]{ (byte)3, (byte)4, (byte)7 }, + new object[]{ (sbyte)-2, (sbyte)-3, (sbyte)-1 }, + new object[]{ (ushort)3, (ushort)4, (ushort)7 }, + new object[]{ (short)-2, (short)-3, (short)-1 }, + new object[]{ (uint)3, (uint)4, (uint)7 }, + new object[]{ (int)-2, (int)-3, (int)-1 }, + new object[]{ (ulong)3, (ulong)4, (ulong)7 }, + new object[]{ (long)-2, (long)-3, (long)-1 }, + }; + + [Theory] + [MemberData(nameof(BitOrData))] + public void CanGenericBitOr(T a, T b, T expected) + where T : unmanaged //, IEqualityComparer (does not compile correctly) + { + // Arrange + + + // Act + var actual = GenericBitOr.BitOr(a, b); + + // Assert + Assert.Equal(expected, actual); + } + + public static IList BitAndData { get; } = new List + { + new object[]{ (byte)168, (byte)44, (byte)40 }, + new object[]{ (sbyte)-88, (sbyte)44, (sbyte)40 }, + new object[]{ (ushort)168, (ushort)44, (ushort)40 }, + new object[]{ (short)-88, (short)44, (short)40 }, + new object[]{ (uint)168, (uint)44, (uint)40 }, + new object[]{ (int)-88, (int)44, (int)40 }, + new object[]{ (ulong)168, (ulong)44, (ulong)40 }, + new object[]{ (long)-88, (long)44, (long)40 }, + }; + + [Theory] + [MemberData(nameof(BitAndData))] + public void CanGenericBitAnd(T a, T b, T expected) + where T : unmanaged //, IEqualityComparer (does not compile correctly) + { + // Arrange + + + // Act + var actual = GenericBitAnd.BitAnd(a, b); + + // Assert + Assert.Equal(expected, actual); + } + + record MyType(int A, string B, TimeSpan C); + + [Fact] + public void CanSerializeAndDeserializeTimeSpan() + { + // Arrange + var expected = new MyType(A: 1, B: "Two", C: TimeSpan.FromSeconds(1)); + + // Act + var jsonString = JsonSerializerHelper.SerializeIndented(expected); + var actual = JsonSerializer.Deserialize(jsonString); + + // Assert + Assert.Equal(expected, actual); + } + + [Fact] + public void CanCastMemory() + { + // Arrange + var values = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 }; + var expected = new int[] { 67305985, 134678021 }; + + // Act + var actual = new CastMemoryManager(values).Memory; + + // Assert + Assert.True(expected.SequenceEqual(actual.ToArray())); + } + + + [Fact] + public void CanDetermineSizeOfNexusDataType() + { + // Arrange + var values = NexusUtilities.GetEnumValues(); + var expected = new[] { 1, 2, 4, 8, 1, 2, 4, 8, 4, 8 }; + + // Act + var actual = values.Select(value => NexusUtilities.SizeOf(value)); + + // Assert + Assert.Equal(expected, actual); } } \ No newline at end of file diff --git a/tests/TestExtensionProject/TestDataSource.cs b/tests/TestExtensionProject/TestDataSource.cs index 92fe7605..58792c56 100644 --- a/tests/TestExtensionProject/TestDataSource.cs +++ b/tests/TestExtensionProject/TestDataSource.cs @@ -2,39 +2,38 @@ using Nexus.DataModel; using Nexus.Extensibility; -namespace TestExtensionProject +namespace TestExtensionProject; + +[ExtensionDescription("A data source for unit tests.", default!, default!)] +public class TestDataSource : IDataSource { - [ExtensionDescription("A data source for unit tests.", default!, default!)] - public class TestDataSource : IDataSource + public Task SetContextAsync(DataSourceContext context, ILogger logger, CancellationToken cancellationToken) { - public Task SetContextAsync(DataSourceContext context, ILogger logger, CancellationToken cancellationToken) - { - throw new NotImplementedException(nameof(SetContextAsync)); - } + throw new NotImplementedException(nameof(SetContextAsync)); + } - public Task GetCatalogRegistrationsAsync(string path, CancellationToken cancellationToken) - { - throw new NotImplementedException(nameof(GetCatalogAsync)); - } + public Task GetCatalogRegistrationsAsync(string path, CancellationToken cancellationToken) + { + throw new NotImplementedException(nameof(GetCatalogAsync)); + } - public Task GetCatalogAsync(string catalogId, CancellationToken cancellationToken) - { - throw new NotImplementedException(nameof(GetCatalogAsync)); - } + public Task GetCatalogAsync(string catalogId, CancellationToken cancellationToken) + { + throw new NotImplementedException(nameof(GetCatalogAsync)); + } - public Task<(DateTime Begin, DateTime End)> GetTimeRangeAsync(string catalogId, CancellationToken cancellationToken) - { - throw new NotImplementedException(nameof(GetTimeRangeAsync)); - } + public Task<(DateTime Begin, DateTime End)> GetTimeRangeAsync(string catalogId, CancellationToken cancellationToken) + { + throw new NotImplementedException(nameof(GetTimeRangeAsync)); + } - public Task GetAvailabilityAsync(string catalogId, DateTime begin, DateTime end, CancellationToken cancellationToken) - { - throw new NotImplementedException(nameof(GetAvailabilityAsync)); - } + public Task GetAvailabilityAsync(string catalogId, DateTime begin, DateTime end, CancellationToken cancellationToken) + { + throw new NotImplementedException(nameof(GetAvailabilityAsync)); + } - public Task ReadAsync(DateTime begin, DateTime end, ReadRequest[] requests, ReadDataHandler readData, IProgress progress, CancellationToken cancellationToken) - { - throw new NotImplementedException(nameof(ReadAsync)); - } + public Task ReadAsync(DateTime begin, DateTime end, ReadRequest[] requests, ReadDataHandler readData, IProgress progress, CancellationToken cancellationToken) + { + throw new NotImplementedException(nameof(ReadAsync)); } } diff --git a/tests/TestExtensionProject/TestDataWriter.cs b/tests/TestExtensionProject/TestDataWriter.cs index cfdd75bc..bd43ee3f 100644 --- a/tests/TestExtensionProject/TestDataWriter.cs +++ b/tests/TestExtensionProject/TestDataWriter.cs @@ -2,29 +2,28 @@ using Nexus.DataModel; using Nexus.Extensibility; -namespace TestExtensionProject +namespace TestExtensionProject; + +[ExtensionDescription("A data writer for unit tests.", default!, default!)] +public class TestDataWriter : IDataWriter { - [ExtensionDescription("A data writer for unit tests.", default!, default!)] - public class TestDataWriter : IDataWriter + public Task CloseAsync(CancellationToken cancellationToken) { - public Task CloseAsync(CancellationToken cancellationToken) - { - throw new NotImplementedException(nameof(CloseAsync)); - } + throw new NotImplementedException(nameof(CloseAsync)); + } - public Task OpenAsync(DateTime fileBegin, TimeSpan filePeriod, TimeSpan samplePeriod, CatalogItem[] catalogItems, CancellationToken cancellationToken) - { - throw new NotImplementedException(nameof(OpenAsync)); - } + public Task OpenAsync(DateTime fileBegin, TimeSpan filePeriod, TimeSpan samplePeriod, CatalogItem[] catalogItems, CancellationToken cancellationToken) + { + throw new NotImplementedException(nameof(OpenAsync)); + } - public Task SetContextAsync(DataWriterContext context, ILogger logger, CancellationToken cancellationToken) - { - throw new NotImplementedException(nameof(SetContextAsync)); - } + public Task SetContextAsync(DataWriterContext context, ILogger logger, CancellationToken cancellationToken) + { + throw new NotImplementedException(nameof(SetContextAsync)); + } - public Task WriteAsync(TimeSpan fileOffset, WriteRequest[] requests, IProgress progress, CancellationToken cancellationToken) - { - throw new NotImplementedException(nameof(WriteAsync)); - } + public Task WriteAsync(TimeSpan fileOffset, WriteRequest[] requests, IProgress progress, CancellationToken cancellationToken) + { + throw new NotImplementedException(nameof(WriteAsync)); } } diff --git a/tests/clients/dotnet-client-tests/ClientTests.cs b/tests/clients/dotnet-client-tests/ClientTests.cs index d3dc6cc6..914a8565 100644 --- a/tests/clients/dotnet-client-tests/ClientTests.cs +++ b/tests/clients/dotnet-client-tests/ClientTests.cs @@ -5,79 +5,78 @@ using Moq.Protected; using Xunit; -namespace Nexus.Api.Tests -{ - public class ClientTests - { - public const string NexusConfigurationHeaderKey = "Nexus-Configuration"; +namespace Nexus.Api.Tests; - [Fact] - public async Task CanAddConfigurationAsync() - { - // Arrange - var messageHandlerMock = new Mock(); - var catalogId = "my-catalog-id"; - var expectedCatalog = new ResourceCatalog(Id: catalogId, default, default); +public class ClientTests +{ + public const string NexusConfigurationHeaderKey = "Nexus-Configuration"; - var actualHeaders = new List?>(); + [Fact] + public async Task CanAddConfigurationAsync() + { + // Arrange + var messageHandlerMock = new Mock(); + var catalogId = "my-catalog-id"; + var expectedCatalog = new ResourceCatalog(Id: catalogId, default, default); - messageHandlerMock - .Protected() - .Setup>( - "SendAsync", - ItExpr.IsAny(), - ItExpr.IsAny()) - .Callback((requestMessage, cancellationToken) => - { - requestMessage.Headers.TryGetValues(NexusConfigurationHeaderKey, out var headers); - actualHeaders.Add(headers); - }) - .ReturnsAsync(() => - { - return new HttpResponseMessage() - { - StatusCode = HttpStatusCode.OK, - Content = new StringContent(JsonSerializer.Serialize(expectedCatalog), Encoding.UTF8, "application/json"), - }; - }); + var actualHeaders = new List?>(); - // -> http client - var httpClient = new HttpClient(messageHandlerMock.Object) + messageHandlerMock + .Protected() + .Setup>( + "SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny()) + .Callback((requestMessage, cancellationToken) => + { + requestMessage.Headers.TryGetValues(NexusConfigurationHeaderKey, out var headers); + actualHeaders.Add(headers); + }) + .ReturnsAsync(() => { - BaseAddress = new Uri("http://localhost") - }; + return new HttpResponseMessage() + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(JsonSerializer.Serialize(expectedCatalog), Encoding.UTF8, "application/json"), + }; + }); - // -> API client - var client = new NexusClient(httpClient); + // -> http client + var httpClient = new HttpClient(messageHandlerMock.Object) + { + BaseAddress = new Uri("http://localhost") + }; - // -> configuration - var configuration = new - { - foo1 = "bar1", - foo2 = "bar2" - }; + // -> API client + var client = new NexusClient(httpClient); - // Act - _ = await client.Catalogs.GetAsync(catalogId); + // -> configuration + var configuration = new + { + foo1 = "bar1", + foo2 = "bar2" + }; - using (var disposable = client.AttachConfiguration(configuration)) - { - _ = await client.Catalogs.GetAsync(catalogId); - } + // Act + _ = await client.Catalogs.GetAsync(catalogId); + using (var disposable = client.AttachConfiguration(configuration)) + { _ = await client.Catalogs.GetAsync(catalogId); + } - // Assert - var encodedJson = Convert.ToBase64String(JsonSerializer.SerializeToUtf8Bytes(configuration)); + _ = await client.Catalogs.GetAsync(catalogId); - Assert.Collection(actualHeaders, - Assert.Null, - headers => - { - Assert.NotNull(headers); - Assert.Collection(headers, header => Assert.Equal(encodedJson, header)); - }, - Assert.Null); - } + // Assert + var encodedJson = Convert.ToBase64String(JsonSerializer.SerializeToUtf8Bytes(configuration)); + + Assert.Collection(actualHeaders, + Assert.Null, + headers => + { + Assert.NotNull(headers); + Assert.Collection(headers, header => Assert.Equal(encodedJson, header)); + }, + Assert.Null); } } diff --git a/tests/extensibility/dotnet-extensibility-tests/DataModelExtensionsTests.cs b/tests/extensibility/dotnet-extensibility-tests/DataModelExtensionsTests.cs index 6882ee24..9c2e4133 100644 --- a/tests/extensibility/dotnet-extensibility-tests/DataModelExtensionsTests.cs +++ b/tests/extensibility/dotnet-extensibility-tests/DataModelExtensionsTests.cs @@ -1,82 +1,81 @@ using Nexus.DataModel; using Xunit; -namespace Nexus.Extensibility.Tests +namespace Nexus.Extensibility.Tests; + +public class DataModelExtensionsTests { - public class DataModelExtensionsTests + [Theory] + [InlineData("00:00:00.0000001", "100_ns")] + [InlineData("00:00:00.0000002", "200_ns")] + [InlineData("00:00:00.0000015", "1500_ns")] + + [InlineData("00:00:00.0000010", "1_us")] + [InlineData("00:00:00.0000100", "10_us")] + [InlineData("00:00:00.0001000", "100_us")] + [InlineData("00:00:00.0015000", "1500_us")] + + [InlineData("00:00:00.0010000", "1_ms")] + [InlineData("00:00:00.0100000", "10_ms")] + [InlineData("00:00:00.1000000", "100_ms")] + [InlineData("00:00:01.5000000", "1500_ms")] + + [InlineData("00:00:01.0000000", "1_s")] + [InlineData("00:00:15.0000000", "15_s")] + + [InlineData("00:01:00.0000000", "1_min")] + [InlineData("00:15:00.0000000", "15_min")] + public void CanCreateUnitStrings(string periodString, string expected) { - [Theory] - [InlineData("00:00:00.0000001", "100_ns")] - [InlineData("00:00:00.0000002", "200_ns")] - [InlineData("00:00:00.0000015", "1500_ns")] - - [InlineData("00:00:00.0000010", "1_us")] - [InlineData("00:00:00.0000100", "10_us")] - [InlineData("00:00:00.0001000", "100_us")] - [InlineData("00:00:00.0015000", "1500_us")] - - [InlineData("00:00:00.0010000", "1_ms")] - [InlineData("00:00:00.0100000", "10_ms")] - [InlineData("00:00:00.1000000", "100_ms")] - [InlineData("00:00:01.5000000", "1500_ms")] - - [InlineData("00:00:01.0000000", "1_s")] - [InlineData("00:00:15.0000000", "15_s")] - - [InlineData("00:01:00.0000000", "1_min")] - [InlineData("00:15:00.0000000", "15_min")] - public void CanCreateUnitStrings(string periodString, string expected) - { - var actual = TimeSpan - .Parse(periodString) - .ToUnitString(); - - Assert.Equal(expected, actual); - } - - [Theory] - [InlineData("100_ns", "00:00:00.0000001")] - [InlineData("200_ns", "00:00:00.0000002")] - [InlineData("1500_ns", "00:00:00.0000015")] - - [InlineData("1_us", "00:00:00.0000010")] - [InlineData("10_us", "00:00:00.0000100")] - [InlineData("100_us", "00:00:00.0001000")] - [InlineData("1500_us", "00:00:00.0015000")] - - [InlineData("1_ms", "00:00:00.0010000")] - [InlineData("10_ms", "00:00:00.0100000")] - [InlineData("100_ms", "00:00:00.1000000")] - [InlineData("1500_ms", "00:00:01.5000000")] - - [InlineData("1_s", "00:00:01.0000000")] - [InlineData("15_s", "00:00:15.0000000")] - - [InlineData("1_min", "00:01:00.0000000")] - [InlineData("15_min", "00:15:00.0000000")] - public void CanParseUnitStrings(string unitString, string expectedPeriodString) - { - var expected = TimeSpan - .Parse(expectedPeriodString); - - var actual = DataModelExtensions.ToSamplePeriod(unitString); - - Assert.Equal(expected, actual); - } - - [Theory] - [InlineData("A and B/C/D", UriKind.Relative, "A and B/C/D")] - [InlineData("A and B/C/D.ext", UriKind.Relative, "A and B/C/D.ext")] - [InlineData(@"file:///C:/A and B", UriKind.Absolute, @"C:/A and B")] - [InlineData(@"file:///C:/A and B/C.ext", UriKind.Absolute, @"C:/A and B/C.ext")] - [InlineData(@"file:///root/A and B", UriKind.Absolute, @"/root/A and B")] - [InlineData(@"file:///root/A and B/C.ext", UriKind.Absolute, @"/root/A and B/C.ext")] - public void CanConvertUriToPath(string uriString, UriKind uriKind, string expected) - { - var uri = new Uri(uriString, uriKind); - var actual = uri.ToPath(); - - Assert.Equal(actual, expected); - } + var actual = TimeSpan + .Parse(periodString) + .ToUnitString(); + + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData("100_ns", "00:00:00.0000001")] + [InlineData("200_ns", "00:00:00.0000002")] + [InlineData("1500_ns", "00:00:00.0000015")] + + [InlineData("1_us", "00:00:00.0000010")] + [InlineData("10_us", "00:00:00.0000100")] + [InlineData("100_us", "00:00:00.0001000")] + [InlineData("1500_us", "00:00:00.0015000")] + + [InlineData("1_ms", "00:00:00.0010000")] + [InlineData("10_ms", "00:00:00.0100000")] + [InlineData("100_ms", "00:00:00.1000000")] + [InlineData("1500_ms", "00:00:01.5000000")] + + [InlineData("1_s", "00:00:01.0000000")] + [InlineData("15_s", "00:00:15.0000000")] + + [InlineData("1_min", "00:01:00.0000000")] + [InlineData("15_min", "00:15:00.0000000")] + public void CanParseUnitStrings(string unitString, string expectedPeriodString) + { + var expected = TimeSpan + .Parse(expectedPeriodString); + + var actual = DataModelExtensions.ToSamplePeriod(unitString); + + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData("A and B/C/D", UriKind.Relative, "A and B/C/D")] + [InlineData("A and B/C/D.ext", UriKind.Relative, "A and B/C/D.ext")] + [InlineData(@"file:///C:/A and B", UriKind.Absolute, @"C:/A and B")] + [InlineData(@"file:///C:/A and B/C.ext", UriKind.Absolute, @"C:/A and B/C.ext")] + [InlineData(@"file:///root/A and B", UriKind.Absolute, @"/root/A and B")] + [InlineData(@"file:///root/A and B/C.ext", UriKind.Absolute, @"/root/A and B/C.ext")] + public void CanConvertUriToPath(string uriString, UriKind uriKind, string expected) + { + var uri = new Uri(uriString, uriKind); + var actual = uri.ToPath(); + + Assert.Equal(actual, expected); } } \ No newline at end of file diff --git a/tests/extensibility/dotnet-extensibility-tests/DataModelFixture.cs b/tests/extensibility/dotnet-extensibility-tests/DataModelFixture.cs index 1e1f90b9..a5737c27 100644 --- a/tests/extensibility/dotnet-extensibility-tests/DataModelFixture.cs +++ b/tests/extensibility/dotnet-extensibility-tests/DataModelFixture.cs @@ -1,146 +1,145 @@ using Nexus.DataModel; -namespace Nexus.Extensibility.Tests +namespace Nexus.Extensibility.Tests; + +public class DataModelFixture { - public class DataModelFixture + public DataModelFixture() { - public DataModelFixture() - { - // catalogs - Catalog0_V0 = new ResourceCatalogBuilder(id: "/A/B/C") - .WithProperty("C_0_A", "A_0") - .WithProperty("C_0_B", "B_0") - .Build(); ; - - Catalog0_V1 = new ResourceCatalogBuilder(id: "/A/B/C") - .WithProperty("C_0_A", "A_1") - .WithProperty("C_0_C", "C_0") - .Build(); - - Catalog0_V2 = new ResourceCatalogBuilder(id: "/A/B/C") - .WithProperty("C_0_C", "C_0") - .Build(); - - Catalog0_Vmerged = new ResourceCatalogBuilder(id: "/A/B/C") - .WithProperty("C_0_A", "A_1") - .WithProperty("C_0_B", "B_0") - .WithProperty("C_0_C", "C_0") - .Build(); - - Catalog0_Vxor = new ResourceCatalogBuilder(id: "/A/B/C") - .WithProperty("C_0_A", "A_0") - .WithProperty("C_0_B", "B_0") - .WithProperty("C_0_C", "C_0") - .Build(); - - // resources - Resource0_V0 = new ResourceBuilder(id: "Resource0") - .WithUnit("U_0") - .WithDescription("D_0") - .WithProperty("R_0_A", "A_0") - .WithProperty("R_0_B", "B_0") - .Build(); - - Resource0_V1 = new ResourceBuilder(id: "Resource0") - .WithUnit("U_1") - .WithDescription("D_1") - .WithGroups("G_1") - .WithProperty("R_0_A", "A_1") - .WithProperty("R_0_C", "C_0") - .Build(); - - Resource0_V2 = new ResourceBuilder(id: "Resource0") - .WithGroups("G_1") - .WithProperty("R_0_C", "C_0") - .Build(); - - Resource0_Vmerged = new ResourceBuilder(id: "Resource0") - .WithUnit("U_1") - .WithDescription("D_1") - .WithProperty("R_0_A", "A_1") - .WithProperty("R_0_B", "B_0") - .WithGroups("G_1") - .WithProperty("R_0_C", "C_0") - .Build(); - - Resource0_Vxor = new ResourceBuilder(id: "Resource0") - .WithUnit("U_0") - .WithDescription("D_0") - .WithProperty("R_0_A", "A_0") - .WithProperty("R_0_B", "B_0") - .WithGroups("G_1") - .WithProperty("R_0_C", "C_0") - .Build(); - - Resource1_V0 = new ResourceBuilder(id: "Resource1") - .WithUnit("U_0") - .WithDescription("D_0") - .WithGroups("G_0") - .WithProperty("R_1_A", "A_0") - .WithProperty("R_1_B", "B_0") - .Build(); - - Resource2_V0 = new ResourceBuilder(id: "Resource2") - .WithUnit("U_0") - .WithDescription("D_0") - .WithGroups("G_0") - .WithProperty("R_2_A", "A_0") - .WithProperty("R_2_B", "B_0") - .Build(); - - Resource3_V0 = new Resource(id: "Resource3"); - Resource3_V1 = Resource3_V0; - Resource3_Vmerged = Resource3_V0; - - Resource4_V0 = new Resource(id: "Resource4"); - Resource4_V1 = Resource4_V0; - Resource4_Vmerged = Resource4_V0; - - // representations - Representation0_V0 = new Representation( - dataType: NexusDataType.FLOAT32, - samplePeriod: TimeSpan.FromMinutes(10)); - - Representation0_V1 = Representation0_V0; - - Representation0_Vmerged = Representation0_V0; - - Representation0_Vxor = Representation0_V0; - - Representation1_V0 = new Representation( - dataType: NexusDataType.FLOAT64, - samplePeriod: TimeSpan.FromMinutes(20)); - - Representation2_V0 = new Representation( - dataType: NexusDataType.UINT16, - samplePeriod: TimeSpan.FromMinutes(100)); - } - - public ResourceCatalog Catalog0_V0 { get; } - public ResourceCatalog Catalog0_V1 { get; } - public ResourceCatalog Catalog0_V2 { get; } - public ResourceCatalog Catalog0_Vmerged { get; } - public ResourceCatalog Catalog0_Vxor { get; } - - public Resource Resource0_V0 { get; } - public Resource Resource0_V1 { get; } - public Resource Resource0_V2 { get; } - public Resource Resource0_Vmerged { get; } - public Resource Resource0_Vxor { get; } - public Resource Resource1_V0 { get; } - public Resource Resource2_V0 { get; } - public Resource Resource3_V0 { get; } - public Resource Resource3_V1 { get; } - public Resource Resource3_Vmerged { get; } - public Resource Resource4_V0 { get; } - public Resource Resource4_V1 { get; } - public Resource Resource4_Vmerged { get; } - - public Representation Representation0_V0 { get; } - public Representation Representation0_V1 { get; } - public Representation Representation0_Vmerged { get; } - public Representation Representation0_Vxor { get; } - public Representation Representation1_V0 { get; } - public Representation Representation2_V0 { get; } + // catalogs + Catalog0_V0 = new ResourceCatalogBuilder(id: "/A/B/C") + .WithProperty("C_0_A", "A_0") + .WithProperty("C_0_B", "B_0") + .Build(); ; + + Catalog0_V1 = new ResourceCatalogBuilder(id: "/A/B/C") + .WithProperty("C_0_A", "A_1") + .WithProperty("C_0_C", "C_0") + .Build(); + + Catalog0_V2 = new ResourceCatalogBuilder(id: "/A/B/C") + .WithProperty("C_0_C", "C_0") + .Build(); + + Catalog0_Vmerged = new ResourceCatalogBuilder(id: "/A/B/C") + .WithProperty("C_0_A", "A_1") + .WithProperty("C_0_B", "B_0") + .WithProperty("C_0_C", "C_0") + .Build(); + + Catalog0_Vxor = new ResourceCatalogBuilder(id: "/A/B/C") + .WithProperty("C_0_A", "A_0") + .WithProperty("C_0_B", "B_0") + .WithProperty("C_0_C", "C_0") + .Build(); + + // resources + Resource0_V0 = new ResourceBuilder(id: "Resource0") + .WithUnit("U_0") + .WithDescription("D_0") + .WithProperty("R_0_A", "A_0") + .WithProperty("R_0_B", "B_0") + .Build(); + + Resource0_V1 = new ResourceBuilder(id: "Resource0") + .WithUnit("U_1") + .WithDescription("D_1") + .WithGroups("G_1") + .WithProperty("R_0_A", "A_1") + .WithProperty("R_0_C", "C_0") + .Build(); + + Resource0_V2 = new ResourceBuilder(id: "Resource0") + .WithGroups("G_1") + .WithProperty("R_0_C", "C_0") + .Build(); + + Resource0_Vmerged = new ResourceBuilder(id: "Resource0") + .WithUnit("U_1") + .WithDescription("D_1") + .WithProperty("R_0_A", "A_1") + .WithProperty("R_0_B", "B_0") + .WithGroups("G_1") + .WithProperty("R_0_C", "C_0") + .Build(); + + Resource0_Vxor = new ResourceBuilder(id: "Resource0") + .WithUnit("U_0") + .WithDescription("D_0") + .WithProperty("R_0_A", "A_0") + .WithProperty("R_0_B", "B_0") + .WithGroups("G_1") + .WithProperty("R_0_C", "C_0") + .Build(); + + Resource1_V0 = new ResourceBuilder(id: "Resource1") + .WithUnit("U_0") + .WithDescription("D_0") + .WithGroups("G_0") + .WithProperty("R_1_A", "A_0") + .WithProperty("R_1_B", "B_0") + .Build(); + + Resource2_V0 = new ResourceBuilder(id: "Resource2") + .WithUnit("U_0") + .WithDescription("D_0") + .WithGroups("G_0") + .WithProperty("R_2_A", "A_0") + .WithProperty("R_2_B", "B_0") + .Build(); + + Resource3_V0 = new Resource(id: "Resource3"); + Resource3_V1 = Resource3_V0; + Resource3_Vmerged = Resource3_V0; + + Resource4_V0 = new Resource(id: "Resource4"); + Resource4_V1 = Resource4_V0; + Resource4_Vmerged = Resource4_V0; + + // representations + Representation0_V0 = new Representation( + dataType: NexusDataType.FLOAT32, + samplePeriod: TimeSpan.FromMinutes(10)); + + Representation0_V1 = Representation0_V0; + + Representation0_Vmerged = Representation0_V0; + + Representation0_Vxor = Representation0_V0; + + Representation1_V0 = new Representation( + dataType: NexusDataType.FLOAT64, + samplePeriod: TimeSpan.FromMinutes(20)); + + Representation2_V0 = new Representation( + dataType: NexusDataType.UINT16, + samplePeriod: TimeSpan.FromMinutes(100)); } + + public ResourceCatalog Catalog0_V0 { get; } + public ResourceCatalog Catalog0_V1 { get; } + public ResourceCatalog Catalog0_V2 { get; } + public ResourceCatalog Catalog0_Vmerged { get; } + public ResourceCatalog Catalog0_Vxor { get; } + + public Resource Resource0_V0 { get; } + public Resource Resource0_V1 { get; } + public Resource Resource0_V2 { get; } + public Resource Resource0_Vmerged { get; } + public Resource Resource0_Vxor { get; } + public Resource Resource1_V0 { get; } + public Resource Resource2_V0 { get; } + public Resource Resource3_V0 { get; } + public Resource Resource3_V1 { get; } + public Resource Resource3_Vmerged { get; } + public Resource Resource4_V0 { get; } + public Resource Resource4_V1 { get; } + public Resource Resource4_Vmerged { get; } + + public Representation Representation0_V0 { get; } + public Representation Representation0_V1 { get; } + public Representation Representation0_Vmerged { get; } + public Representation Representation0_Vxor { get; } + public Representation Representation1_V0 { get; } + public Representation Representation2_V0 { get; } } diff --git a/tests/extensibility/dotnet-extensibility-tests/DataModelTests.cs b/tests/extensibility/dotnet-extensibility-tests/DataModelTests.cs index f27852f4..10ff118f 100644 --- a/tests/extensibility/dotnet-extensibility-tests/DataModelTests.cs +++ b/tests/extensibility/dotnet-extensibility-tests/DataModelTests.cs @@ -2,318 +2,317 @@ using System.Text.Json; using Xunit; -namespace Nexus.Extensibility.Tests +namespace Nexus.Extensibility.Tests; + +public class DataModelTests : IClassFixture { - public class DataModelTests : IClassFixture + private readonly DataModelFixture _fixture; + + public DataModelTests(DataModelFixture fixture) { - private readonly DataModelFixture _fixture; + _fixture = fixture; + } - public DataModelTests(DataModelFixture fixture) - { - _fixture = fixture; - } + [Theory] + + // valid + [InlineData("/a", true)] + [InlineData("/_a", true)] + [InlineData("/ab_c", true)] + [InlineData("/a9_b/c__99", true)] + + // invalid + [InlineData("", false)] + [InlineData("/", false)] + [InlineData("/a/", false)] + [InlineData("/9", false)] + [InlineData("a", false)] + public void CanValidateCatalogId(string id, bool isValid) + { + if (isValid) + _ = new ResourceCatalog(id: id); - [Theory] - - // valid - [InlineData("/a", true)] - [InlineData("/_a", true)] - [InlineData("/ab_c", true)] - [InlineData("/a9_b/c__99", true)] - - // invalid - [InlineData("", false)] - [InlineData("/", false)] - [InlineData("/a/", false)] - [InlineData("/9", false)] - [InlineData("a", false)] - public void CanValidateCatalogId(string id, bool isValid) - { - if (isValid) - _ = new ResourceCatalog(id: id); + else + Assert.Throws(() => new ResourceCatalog(id: id)); + } - else - Assert.Throws(() => new ResourceCatalog(id: id)); - } + [Theory] + + // valid + [InlineData("_temp", true)] + [InlineData("temp", true)] + [InlineData("Temp", true)] + [InlineData("Temp_1", true)] + + // invalid + [InlineData("", false)] + [InlineData("1temp", false)] + [InlineData("teßp", false)] + [InlineData("ª♫", false)] + [InlineData("tem p", false)] + [InlineData("tem-p", false)] + [InlineData("tem*p", false)] + public void CanValidateResourceId(string id, bool isValid) + { + if (isValid) + _ = new Resource(id: id); - [Theory] - - // valid - [InlineData("_temp", true)] - [InlineData("temp", true)] - [InlineData("Temp", true)] - [InlineData("Temp_1", true)] - - // invalid - [InlineData("", false)] - [InlineData("1temp", false)] - [InlineData("teßp", false)] - [InlineData("ª♫", false)] - [InlineData("tem p", false)] - [InlineData("tem-p", false)] - [InlineData("tem*p", false)] - public void CanValidateResourceId(string id, bool isValid) - { - if (isValid) - _ = new Resource(id: id); + else + Assert.Throws(() => new Resource(id: id)); + } - else - Assert.Throws(() => new Resource(id: id)); - } + [Theory] + [InlineData("00:01:00", true)] + [InlineData("00:00:00", false)] + public void CanValidateRepresentationSamplePeriod(string samplePeriodString, bool isValid) + { + var samplePeriod = TimeSpan.Parse(samplePeriodString); - [Theory] - [InlineData("00:01:00", true)] - [InlineData("00:00:00", false)] - public void CanValidateRepresentationSamplePeriod(string samplePeriodString, bool isValid) - { - var samplePeriod = TimeSpan.Parse(samplePeriodString); + if (isValid) + _ = new Representation( + dataType: NexusDataType.FLOAT64, + samplePeriod: samplePeriod); - if (isValid) - _ = new Representation( - dataType: NexusDataType.FLOAT64, - samplePeriod: samplePeriod); + else + Assert.Throws(() => new Representation( + dataType: NexusDataType.FLOAT64, + samplePeriod: samplePeriod)); + } - else - Assert.Throws(() => new Representation( - dataType: NexusDataType.FLOAT64, - samplePeriod: samplePeriod)); - } + [Theory] + [InlineData(30, true)] + [InlineData(-1, false)] + public void CanValidateRepresentationKind(int numericalKind, bool isValid) + { + var kind = (RepresentationKind)numericalKind; - [Theory] - [InlineData(30, true)] - [InlineData(-1, false)] - public void CanValidateRepresentationKind(int numericalKind, bool isValid) - { - var kind = (RepresentationKind)numericalKind; - - if (isValid) - _ = new Representation( - dataType: NexusDataType.FLOAT64, - samplePeriod: TimeSpan.FromSeconds(1), - parameters: default, - kind: kind); - - else - Assert.Throws(() => new Representation( - dataType: NexusDataType.FLOAT64, - samplePeriod: TimeSpan.FromSeconds(1), - parameters: default, - kind: kind)); - } + if (isValid) + _ = new Representation( + dataType: NexusDataType.FLOAT64, + samplePeriod: TimeSpan.FromSeconds(1), + parameters: default, + kind: kind); - [Theory] - [InlineData(NexusDataType.FLOAT32, true)] - [InlineData((NexusDataType)0, false)] - [InlineData((NexusDataType)9999, false)] - public void CanValidateRepresentationDataType(NexusDataType dataType, bool isValid) - { - if (isValid) - _ = new Representation( - dataType: dataType, - samplePeriod: TimeSpan.FromSeconds(1)); - - else - Assert.Throws(() => new Representation( - dataType: dataType, - samplePeriod: TimeSpan.FromSeconds(1))); - } + else + Assert.Throws(() => new Representation( + dataType: NexusDataType.FLOAT64, + samplePeriod: TimeSpan.FromSeconds(1), + parameters: default, + kind: kind)); + } - [Theory] - [InlineData("00:00:01", "MeanPolarDeg", "1_s_mean_polar_deg")] - public void CanInferRepresentationId(string samplePeriodString, string kindString, string expected) - { - var kind = Enum.Parse(kindString); - var samplePeriod = TimeSpan.Parse(samplePeriodString); + [Theory] + [InlineData(NexusDataType.FLOAT32, true)] + [InlineData((NexusDataType)0, false)] + [InlineData((NexusDataType)9999, false)] + public void CanValidateRepresentationDataType(NexusDataType dataType, bool isValid) + { + if (isValid) + _ = new Representation( + dataType: dataType, + samplePeriod: TimeSpan.FromSeconds(1)); + + else + Assert.Throws(() => new Representation( + dataType: dataType, + samplePeriod: TimeSpan.FromSeconds(1))); + } - var representation = new Representation( - dataType: NexusDataType.FLOAT32, - samplePeriod: samplePeriod, - parameters: default, - kind: kind); + [Theory] + [InlineData("00:00:01", "MeanPolarDeg", "1_s_mean_polar_deg")] + public void CanInferRepresentationId(string samplePeriodString, string kindString, string expected) + { + var kind = Enum.Parse(kindString); + var samplePeriod = TimeSpan.Parse(samplePeriodString); - var actual = representation.Id; + var representation = new Representation( + dataType: NexusDataType.FLOAT32, + samplePeriod: samplePeriod, + parameters: default, + kind: kind); - Assert.Equal(expected, actual); - } + var actual = representation.Id; - [Fact] - public void CanMergeCatalogs() - { - // arrange - - // prepare catalog 0 - var representation0_V0 = _fixture.Representation0_V0; - var representation1_V0 = _fixture.Representation1_V0; - var resource0_V0 = _fixture.Resource0_V0 with { Representations = new List() { representation0_V0, representation1_V0 } }; - var resource1_V0 = _fixture.Resource1_V0 with { Representations = default }; - var resource3_V0 = _fixture.Resource3_V0 with { Representations = default }; - var resource4_V0 = _fixture.Resource4_V0 with { Representations = new List() { representation0_V0, representation1_V0 } }; - var catalog0_V0 = _fixture.Catalog0_V0 with { Resources = new List() { resource0_V0, resource1_V0, resource3_V0, resource4_V0 } }; - - // prepare catalog 1 - var representation0_V1 = _fixture.Representation0_V1; - var representation2_V0 = _fixture.Representation2_V0; - var resource0_V1 = _fixture.Resource0_V1 with { Representations = new List() { representation0_V1, representation2_V0 } }; - var resource2_V0 = _fixture.Resource2_V0 with { Representations = default }; - var resource3_V1 = _fixture.Resource3_V1 with { Representations = new List() { representation0_V1, representation1_V0 } }; - var resource4_V1 = _fixture.Resource4_V1 with { Representations = default }; - var catalog0_V1 = _fixture.Catalog0_V1 with { Resources = new List() { resource0_V1, resource2_V0, resource3_V1, resource4_V1 } }; - - // prepare merged - var representation0_Vnew = _fixture.Representation0_Vmerged; - var resource0_Vnew = _fixture.Resource0_Vmerged with { Representations = new List() { representation0_Vnew, representation1_V0, representation2_V0 } }; - var resource3_Vnew = _fixture.Resource3_Vmerged with { Representations = new List() { representation0_V1, representation1_V0 } }; - var resource4_Vnew = _fixture.Resource4_Vmerged with { Representations = new List() { representation0_V0, representation1_V0 } }; - var catalog0_Vnew = _fixture.Catalog0_Vmerged with { Resources = new List() { resource0_Vnew, resource1_V0, resource3_Vnew, resource4_Vnew, resource2_V0 } }; - - // act - var catalog0_actual = catalog0_V0.Merge(catalog0_V1); - - // assert - var expected = JsonSerializer.Serialize(catalog0_Vnew); - var actual = JsonSerializer.Serialize(catalog0_actual); - - Assert.Equal(expected, actual); - } + Assert.Equal(expected, actual); + } - [Fact] - public void CatalogMergeThrowsForNonMatchingIdentifiers() - { - // Arrange - var catalog1 = new ResourceCatalog(id: "/C1"); - var catalog2 = new ResourceCatalog(id: "/C2"); + [Fact] + public void CanMergeCatalogs() + { + // arrange + + // prepare catalog 0 + var representation0_V0 = _fixture.Representation0_V0; + var representation1_V0 = _fixture.Representation1_V0; + var resource0_V0 = _fixture.Resource0_V0 with { Representations = new List() { representation0_V0, representation1_V0 } }; + var resource1_V0 = _fixture.Resource1_V0 with { Representations = default }; + var resource3_V0 = _fixture.Resource3_V0 with { Representations = default }; + var resource4_V0 = _fixture.Resource4_V0 with { Representations = new List() { representation0_V0, representation1_V0 } }; + var catalog0_V0 = _fixture.Catalog0_V0 with { Resources = new List() { resource0_V0, resource1_V0, resource3_V0, resource4_V0 } }; + + // prepare catalog 1 + var representation0_V1 = _fixture.Representation0_V1; + var representation2_V0 = _fixture.Representation2_V0; + var resource0_V1 = _fixture.Resource0_V1 with { Representations = new List() { representation0_V1, representation2_V0 } }; + var resource2_V0 = _fixture.Resource2_V0 with { Representations = default }; + var resource3_V1 = _fixture.Resource3_V1 with { Representations = new List() { representation0_V1, representation1_V0 } }; + var resource4_V1 = _fixture.Resource4_V1 with { Representations = default }; + var catalog0_V1 = _fixture.Catalog0_V1 with { Resources = new List() { resource0_V1, resource2_V0, resource3_V1, resource4_V1 } }; + + // prepare merged + var representation0_Vnew = _fixture.Representation0_Vmerged; + var resource0_Vnew = _fixture.Resource0_Vmerged with { Representations = new List() { representation0_Vnew, representation1_V0, representation2_V0 } }; + var resource3_Vnew = _fixture.Resource3_Vmerged with { Representations = new List() { representation0_V1, representation1_V0 } }; + var resource4_Vnew = _fixture.Resource4_Vmerged with { Representations = new List() { representation0_V0, representation1_V0 } }; + var catalog0_Vnew = _fixture.Catalog0_Vmerged with { Resources = new List() { resource0_Vnew, resource1_V0, resource3_Vnew, resource4_Vnew, resource2_V0 } }; + + // act + var catalog0_actual = catalog0_V0.Merge(catalog0_V1); + + // assert + var expected = JsonSerializer.Serialize(catalog0_Vnew); + var actual = JsonSerializer.Serialize(catalog0_actual); + + Assert.Equal(expected, actual); + } - // Act - void action() => catalog1.Merge(catalog2); + [Fact] + public void CatalogMergeThrowsForNonMatchingIdentifiers() + { + // Arrange + var catalog1 = new ResourceCatalog(id: "/C1"); + var catalog2 = new ResourceCatalog(id: "/C2"); - // Assert - Assert.Throws(action); - } + // Act + void action() => catalog1.Merge(catalog2); - [Fact] - public void CatalogConstructorThrowsForNonUniqueResource() - { - // Act - static void action() - { - var catalog = new ResourceCatalog( - id: "/C", - resources: new List() - { - new Resource(id: "R1"), - new Resource(id: "R2"), - new Resource(id: "R2") - }); - } - - // Assert - Assert.Throws(action); - } + // Assert + Assert.Throws(action); + } - [Fact] - public void ResourceMergeThrowsForNonEqualRepresentations() + [Fact] + public void CatalogConstructorThrowsForNonUniqueResource() + { + // Act + static void action() { - // Arrange - var resource1 = new Resource( - id: "myresource", - representations: new List() + var catalog = new ResourceCatalog( + id: "/C", + resources: new List() { - new Representation(dataType: NexusDataType.FLOAT32, samplePeriod: TimeSpan.FromSeconds(1)) + new Resource(id: "R1"), + new Resource(id: "R2"), + new Resource(id: "R2") }); + } - var resource2 = new Resource( - id: "myresource", - representations: new List() - { - new Representation(dataType: NexusDataType.FLOAT64, samplePeriod: TimeSpan.FromSeconds(1)), - new Representation(dataType: NexusDataType.FLOAT32, samplePeriod: TimeSpan.FromSeconds(2)), - new Representation(dataType: NexusDataType.FLOAT32, samplePeriod: TimeSpan.FromSeconds(3)) - }); + // Assert + Assert.Throws(action); + } - // Act - void action() => resource1.Merge(resource2); + [Fact] + public void ResourceMergeThrowsForNonEqualRepresentations() + { + // Arrange + var resource1 = new Resource( + id: "myresource", + representations: new List() + { + new Representation(dataType: NexusDataType.FLOAT32, samplePeriod: TimeSpan.FromSeconds(1)) + }); - // Assert - Assert.Throws(action); - } + var resource2 = new Resource( + id: "myresource", + representations: new List() + { + new Representation(dataType: NexusDataType.FLOAT64, samplePeriod: TimeSpan.FromSeconds(1)), + new Representation(dataType: NexusDataType.FLOAT32, samplePeriod: TimeSpan.FromSeconds(2)), + new Representation(dataType: NexusDataType.FLOAT32, samplePeriod: TimeSpan.FromSeconds(3)) + }); - [Fact] - public void ResourceMergeThrowsForNonMatchingIdentifiers() - { - // Arrange - var resource1 = new Resource(id: "R1"); - var resource2 = new Resource(id: "R2"); + // Act + void action() => resource1.Merge(resource2); - // Act - void action() => resource1.Merge(resource2); + // Assert + Assert.Throws(action); + } - // Assert - Assert.Throws(action); - } + [Fact] + public void ResourceMergeThrowsForNonMatchingIdentifiers() + { + // Arrange + var resource1 = new Resource(id: "R1"); + var resource2 = new Resource(id: "R2"); - [Fact] - public void CanFindCatalogItem() - { - var representation = new Representation( - dataType: NexusDataType.FLOAT32, - samplePeriod: TimeSpan.FromSeconds(1)); + // Act + void action() => resource1.Merge(resource2); - var resource = new Resource(id: "Resource1", representations: new List() { representation }); - var catalog = new ResourceCatalog(id: "/A/B/C", resources: new List() { resource }); + // Assert + Assert.Throws(action); + } - var catalogItem = new CatalogItem( - catalog with { Resources = default }, - resource with { Representations = default }, - representation, - Parameters: default); + [Fact] + public void CanFindCatalogItem() + { + var representation = new Representation( + dataType: NexusDataType.FLOAT32, + samplePeriod: TimeSpan.FromSeconds(1)); - var foundCatalogItem = catalog.Find(catalogItem.ToPath()); + var resource = new Resource(id: "Resource1", representations: new List() { representation }); + var catalog = new ResourceCatalog(id: "/A/B/C", resources: new List() { resource }); - Assert.Equal(catalogItem, foundCatalogItem); - } + var catalogItem = new CatalogItem( + catalog with { Resources = default }, + resource with { Representations = default }, + representation, + Parameters: default); - [Fact] - public void CanTryFindCatalogItem() - { - var representation = new Representation( - dataType: NexusDataType.FLOAT32, - samplePeriod: TimeSpan.FromSeconds(1)); + var foundCatalogItem = catalog.Find(catalogItem.ToPath()); - var resource = new Resource(id: "Resource1", representations: new List() { representation }); - var catalog = new ResourceCatalog(id: "/A/B/C", resources: new List() { resource }); + Assert.Equal(catalogItem, foundCatalogItem); + } - var catalogItem = new CatalogItem( - catalog with { Resources = default }, - resource with { Representations = default }, - representation, - Parameters: default); + [Fact] + public void CanTryFindCatalogItem() + { + var representation = new Representation( + dataType: NexusDataType.FLOAT32, + samplePeriod: TimeSpan.FromSeconds(1)); - _ = DataModelUtilities.TryParseResourcePath(catalogItem.ToPath(), out var parseResult); - var success = catalog.TryFind(parseResult!, out var foundCatalogItem1); + var resource = new Resource(id: "Resource1", representations: new List() { representation }); + var catalog = new ResourceCatalog(id: "/A/B/C", resources: new List() { resource }); - Assert.Equal(catalogItem, foundCatalogItem1); - Assert.True(success); - } + var catalogItem = new CatalogItem( + catalog with { Resources = default }, + resource with { Representations = default }, + representation, + Parameters: default); - [Theory] - [InlineData("/A/B/C/Resource1/1_s(param1=2)")] - [InlineData("/A/B/C/Resource2/1_s")] - [InlineData("/A/B/D/Resource1/1_s")] - [InlineData("/A/B/D/Resource1/10_s#base=2_s")] - public void ThrowsForInvalidResourcePath(string resourcePath) - { - var representation = new Representation( - dataType: NexusDataType.FLOAT32, - samplePeriod: TimeSpan.FromSeconds(1), - kind: RepresentationKind.Original, - parameters: default); - - var resource = new Resource(id: "Resource1", representations: new List() { representation }); - var catalog = new ResourceCatalog(id: "/A/B/C", resources: new List() { resource }); - var catalogItem = new CatalogItem(catalog, resource, representation, Parameters: default); - - void action() => catalog.Find(resourcePath); - Assert.Throws(action); - } + _ = DataModelUtilities.TryParseResourcePath(catalogItem.ToPath(), out var parseResult); + var success = catalog.TryFind(parseResult!, out var foundCatalogItem1); + + Assert.Equal(catalogItem, foundCatalogItem1); + Assert.True(success); + } + + [Theory] + [InlineData("/A/B/C/Resource1/1_s(param1=2)")] + [InlineData("/A/B/C/Resource2/1_s")] + [InlineData("/A/B/D/Resource1/1_s")] + [InlineData("/A/B/D/Resource1/10_s#base=2_s")] + public void ThrowsForInvalidResourcePath(string resourcePath) + { + var representation = new Representation( + dataType: NexusDataType.FLOAT32, + samplePeriod: TimeSpan.FromSeconds(1), + kind: RepresentationKind.Original, + parameters: default); + + var resource = new Resource(id: "Resource1", representations: new List() { representation }); + var catalog = new ResourceCatalog(id: "/A/B/C", resources: new List() { resource }); + var catalogItem = new CatalogItem(catalog, resource, representation, Parameters: default); + + void action() => catalog.Find(resourcePath); + Assert.Throws(action); } } \ No newline at end of file From aea95b8360625d26a6d2d092c64a496564d29068 Mon Sep 17 00:00:00 2001 From: Apollo3zehn Date: Thu, 7 Mar 2024 21:23:23 +0100 Subject: [PATCH 3/5] Remove regions --- src/Nexus.UI/Charts/Chart.razor.cs | 18 +------------- src/Nexus.UI/Core/AppState.cs | 20 ---------------- src/Nexus.UI/ViewModels/SettingsViewModel.cs | 10 +------- src/Nexus/API/ArtifactsController.cs | 12 ---------- src/Nexus/API/CatalogsController.cs | 12 ---------- src/Nexus/API/DataController.cs | 8 ------- src/Nexus/API/JobsController.cs | 8 ------- src/Nexus/API/PackageReferencesController.cs | 12 ---------- src/Nexus/API/SourcesController.cs | 8 ------- src/Nexus/API/SystemController.cs | 12 ---------- src/Nexus/API/UsersController.cs | 8 ------- src/Nexus/API/WritersController.cs | 12 ---------- src/Nexus/Core/AppState.cs | 8 ------- .../DataSource/DataSourceController.cs | 24 ------------------- .../DataSource/DataSourceDoubleStream.cs | 16 ------------- .../DataWriter/DataWriterController.cs | 16 ------------- src/Nexus/Extensions/Sources/Sample.cs | 18 +++----------- src/Nexus/Extensions/Writers/Csv.cs | 16 ------------- .../PackageManagement/PackageController.cs | 16 ------------- .../PackageManagement/PackageLoadContext.cs | 12 ---------- src/Nexus/Services/AppStateManager.cs | 16 ------------- src/Nexus/Services/CatalogManager.cs | 16 ------------- src/Nexus/Services/DataService.cs | 16 ------------- src/Nexus/Services/ExtensionHive.cs | 12 ---------- src/Nexus/Services/JobService.cs | 12 ---------- .../DataModel/Representation.cs | 16 ------------- .../DataModel/Resource.cs | 16 ------------- .../DataModel/ResourceBuilder.cs | 12 ---------- .../DataModel/ResourceCatalog.cs | 16 ------------- .../DataModel/ResourceCatalogBuilder.cs | 12 ---------- .../DataSource/SimpleDataSource.cs | 8 ------- .../Other/PackageControllerTests.cs | 4 ---- 32 files changed, 5 insertions(+), 417 deletions(-) diff --git a/src/Nexus.UI/Charts/Chart.razor.cs b/src/Nexus.UI/Charts/Chart.razor.cs index b3c3a5d1..0e9d358d 100644 --- a/src/Nexus.UI/Charts/Chart.razor.cs +++ b/src/Nexus.UI/Charts/Chart.razor.cs @@ -9,8 +9,6 @@ namespace Nexus.UI.Charts; public partial class Chart : IDisposable { - #region Fields - private SKGLView _skiaView = default!; private readonly string _chartId = Guid.NewGuid().ToString(); private Dictionary _axesMap = default!; @@ -40,7 +38,7 @@ public partial class Chart : IDisposable private const float AXIS_MARGIN_RIGHT = 5; private const float HALF_LINE_HEIGHT = 3.5f; - private readonly int[] _factors = new int[] { 2, 5, 10, 20, 50 }; + private readonly int[] _factors = [2, 5, 10, 20, 50]; /* Time-Axis */ private const float TIME_AXIS_MARGIN_TOP = 15; @@ -52,10 +50,6 @@ public partial class Chart : IDisposable private bool _beginAtZero; private readonly SKColor[] _colors; - #endregion - - #region Constructors - public Chart() { _dotNetHelper = DotNetObjectReference.Create(this); @@ -122,10 +116,6 @@ public Chart() }; } - #endregion - - #region Properties - [Inject] public TypeFaceService TypeFaceService { get; set; } = default!; @@ -160,10 +150,6 @@ public bool BeginAtZero } } - #endregion - - #region Callbacks - protected override void OnInitialized() { /* line series color */ @@ -276,8 +262,6 @@ private void ToggleSeriesEnabled(LineSeries series) _skiaView.Invalidate(); } - #endregion - #region Draw private void PaintSurface(SKPaintGLSurfaceEventArgs e) diff --git a/src/Nexus.UI/Core/AppState.cs b/src/Nexus.UI/Core/AppState.cs index 9b7cc5a2..75e75389 100644 --- a/src/Nexus.UI/Core/AppState.cs +++ b/src/Nexus.UI/Core/AppState.cs @@ -11,14 +11,8 @@ namespace Nexus.UI.Core; public class AppState : INotifyPropertyChanged { - #region Events - public event PropertyChangedEventHandler? PropertyChanged; - #endregion - - #region Fields - private ResourceCatalogViewModel? _selectedCatalog; private ViewState _viewState = ViewState.Normal; private ExportParameters _exportParameters = default!; @@ -31,10 +25,6 @@ public class AppState : INotifyPropertyChanged private readonly IJSInProcessRuntime _jsRuntime; private IDisposable? _requestConfiguration; - #endregion - - #region Constructors - public AppState( bool isDemo, IReadOnlyList authenticationSchemes, @@ -88,10 +78,6 @@ public AppState( BeginAtZero = true; } - #endregion - - #region Properties - public bool IsDemo { get; } public ViewState ViewState @@ -196,10 +182,6 @@ public string? SearchString public ObservableCollection Jobs { get; set; } = new ObservableCollection(); - #endregion - - #region Methods - public void AddJob(JobViewModel job) { if (Jobs.Count >= 20) @@ -415,6 +397,4 @@ public void ClearRequestConfiguration() { _jsRuntime.InvokeVoid("nexus.util.clearSetting", Constants.REQUEST_CONFIGURATION_KEY); } - - #endregion } \ No newline at end of file diff --git a/src/Nexus.UI/ViewModels/SettingsViewModel.cs b/src/Nexus.UI/ViewModels/SettingsViewModel.cs index 34df75c3..db3a4056 100644 --- a/src/Nexus.UI/ViewModels/SettingsViewModel.cs +++ b/src/Nexus.UI/ViewModels/SettingsViewModel.cs @@ -8,22 +8,14 @@ namespace Nexus.UI.ViewModels; public class SettingsViewModel : INotifyPropertyChanged { - #region Events - public event PropertyChangedEventHandler? PropertyChanged; - #endregion - - #region Fields - private TimeSpan _samplePeriod = TimeSpan.FromSeconds(1); private readonly AppState _appState; private readonly INexusClient _client; private readonly IJSInProcessRuntime _jsRuntime; private List _selectedCatalogItems = new(); - #endregion - public SettingsViewModel(AppState appState, IJSInProcessRuntime jsRuntime, INexusClient client) { _appState = appState; @@ -33,7 +25,7 @@ public SettingsViewModel(AppState appState, IJSInProcessRuntime jsRuntime, INexu InitializeTask = new Lazy(InitializeAsync); } - private string DefaultFileType { get; set; } + private string DefaultFileType { get; set; } = default!; public DateTime Begin { diff --git a/src/Nexus/API/ArtifactsController.cs b/src/Nexus/API/ArtifactsController.cs index aefaada7..e04aea59 100644 --- a/src/Nexus/API/ArtifactsController.cs +++ b/src/Nexus/API/ArtifactsController.cs @@ -15,24 +15,14 @@ internal class ArtifactsController : ControllerBase { // GET /api/artifacts/{artifactId} - #region Fields - public IDatabaseService _databaseService; - #endregion - - #region Constructors - public ArtifactsController( IDatabaseService databaseService) { _databaseService = databaseService; } - #endregion - - #region Methods - /// /// Gets the specified artifact. /// @@ -53,6 +43,4 @@ public ActionResult return NotFound($"Could not find artifact {artifactId}."); } } - - #endregion } diff --git a/src/Nexus/API/CatalogsController.cs b/src/Nexus/API/CatalogsController.cs index 6ac2239d..8e50f36c 100644 --- a/src/Nexus/API/CatalogsController.cs +++ b/src/Nexus/API/CatalogsController.cs @@ -38,16 +38,10 @@ internal class CatalogsController : ControllerBase // GET /api/catalogs/{catalogId}/metadata // PUT /api/catalogs/{catalogId}/metadata - #region Fields - private readonly AppState _appState; private readonly IDatabaseService _databaseService; private readonly IDataControllerService _dataControllerService; - #endregion - - #region Constructors - public CatalogsController( AppState appState, IDatabaseService databaseService, @@ -58,10 +52,6 @@ public CatalogsController( _dataControllerService = dataControllerService; } - #endregion - - #region Methods - /// /// Searches for the given resource paths and returns the corresponding catalog items. /// @@ -577,6 +567,4 @@ private async Task ProtectCatalogNonGenericAsync( return NotFound(catalogId); } } - - #endregion } diff --git a/src/Nexus/API/DataController.cs b/src/Nexus/API/DataController.cs index 7e4a58c6..23b4969e 100644 --- a/src/Nexus/API/DataController.cs +++ b/src/Nexus/API/DataController.cs @@ -18,22 +18,14 @@ internal class DataController : ControllerBase { // GET /api/data - #region Fields - private readonly IDataService _dataService; - #endregion - - #region Constructors - public DataController( IDataService dataService) { _dataService = dataService; } - #endregion - /// /// Gets the requested data. /// diff --git a/src/Nexus/API/JobsController.cs b/src/Nexus/API/JobsController.cs index 51cb327f..99e09471 100644 --- a/src/Nexus/API/JobsController.cs +++ b/src/Nexus/API/JobsController.cs @@ -24,18 +24,12 @@ internal class JobsController : ControllerBase // POST /jobs/load-packages // POST /jobs/clear-cache - #region Fields - private readonly AppStateManager _appStateManager; private readonly ILogger _logger; private readonly IServiceProvider _serviceProvider; private readonly Serilog.IDiagnosticContext _diagnosticContext; private readonly IJobService _jobService; - #endregion - - #region Constructors - public JobsController( AppStateManager appStateManager, IJobService jobService, @@ -50,8 +44,6 @@ public JobsController( _logger = logger; } - #endregion - #region Jobs Management /// diff --git a/src/Nexus/API/PackageReferencesController.cs b/src/Nexus/API/PackageReferencesController.cs index 79f9846d..022d801e 100644 --- a/src/Nexus/API/PackageReferencesController.cs +++ b/src/Nexus/API/PackageReferencesController.cs @@ -19,16 +19,10 @@ internal class PackageReferencesController : ControllerBase // DELETE /api/packagereferences/{packageReferenceId} // GET /api/packagereferences/{packageReferenceId}/versions - #region Fields - private readonly AppState _appState; private readonly AppStateManager _appStateManager; private readonly IExtensionHive _extensionHive; - #endregion - - #region Constructors - public PackageReferencesController( AppState appState, AppStateManager appStateManager, @@ -39,10 +33,6 @@ public PackageReferencesController( _extensionHive = extensionHive; } - #endregion - - #region Methods - /// /// Gets the list of package references. /// @@ -105,6 +95,4 @@ public async Task> GetVersionsAsync( return result; } - - #endregion } \ No newline at end of file diff --git a/src/Nexus/API/SourcesController.cs b/src/Nexus/API/SourcesController.cs index 72c5df84..9f3954ec 100644 --- a/src/Nexus/API/SourcesController.cs +++ b/src/Nexus/API/SourcesController.cs @@ -24,16 +24,10 @@ internal class SourcesController : ControllerBase // POST /api/sources/registrations // DELETE /api/sources/registrations/{registrationId} - #region Fields - private readonly AppState _appState; private readonly AppStateManager _appStateManager; private readonly IExtensionHive _extensionHive; - #endregion - - #region Constructors - public SourcesController( AppState appState, AppStateManager appStateManager, @@ -44,8 +38,6 @@ public SourcesController( _extensionHive = extensionHive; } - #endregion - /// /// Gets the list of source descriptions. /// diff --git a/src/Nexus/API/SystemController.cs b/src/Nexus/API/SystemController.cs index b80a7b1a..064f37a7 100644 --- a/src/Nexus/API/SystemController.cs +++ b/src/Nexus/API/SystemController.cs @@ -24,16 +24,10 @@ internal class SystemController : ControllerBase // [privileged] // PUT /api/system/configuration - #region Fields - private readonly AppState _appState; private readonly AppStateManager _appStateManager; private readonly GeneralOptions _generalOptions; - #endregion - - #region Constructors - public SystemController( AppState appState, AppStateManager appStateManager, @@ -44,10 +38,6 @@ public SystemController( _appStateManager = appStateManager; } - #endregion - - #region Methods - /// /// Gets the default file type. /// @@ -84,6 +74,4 @@ public Task SetConfigurationAsync(IReadOnlyDictionary? conf { return _appStateManager.PutSystemConfigurationAsync(configuration); } - - #endregion } diff --git a/src/Nexus/API/UsersController.cs b/src/Nexus/API/UsersController.cs index 16a5c513..ca37cbfb 100644 --- a/src/Nexus/API/UsersController.cs +++ b/src/Nexus/API/UsersController.cs @@ -47,17 +47,11 @@ internal class UsersController : ControllerBase // GET /api/users/{userId}/tokens - #region Fields - private readonly IDBService _dbService; private readonly ITokenService _tokenService; private readonly SecurityOptions _securityOptions; private readonly ILogger _logger; - #endregion - - #region Constructors - public UsersController( IDBService dBService, ITokenService tokenService, @@ -70,8 +64,6 @@ public UsersController( _logger = logger; } - #endregion - #region Anonymous /// diff --git a/src/Nexus/API/WritersController.cs b/src/Nexus/API/WritersController.cs index db609ac1..3c2aeb12 100644 --- a/src/Nexus/API/WritersController.cs +++ b/src/Nexus/API/WritersController.cs @@ -15,24 +15,14 @@ internal class WritersController : ControllerBase { // GET /api/writers/descriptions - #region Fields - private readonly AppState _appState; - #endregion - - #region Constructors - public WritersController( AppState appState) { _appState = appState; } - #endregion - - #region Methods - /// /// Gets the list of writer descriptions. /// @@ -41,6 +31,4 @@ public List GetDescriptions() { return _appState.DataWriterDescriptions; } - - #endregion } diff --git a/src/Nexus/Core/AppState.cs b/src/Nexus/Core/AppState.cs index 6eecd72d..53cbeed6 100644 --- a/src/Nexus/Core/AppState.cs +++ b/src/Nexus/Core/AppState.cs @@ -6,8 +6,6 @@ namespace Nexus.Core; internal class AppState { - #region Constructors - public AppState() { var entryAssembly = Assembly.GetEntryAssembly()!; @@ -16,10 +14,6 @@ public AppState() Version = version.ToString(); } - #endregion - - #region Properties - General - public ConcurrentDictionary> ResourceCache { get; } = new ConcurrentDictionary>(); @@ -33,6 +27,4 @@ public AppState() public CatalogState CatalogState { get; set; } = default!; public List DataWriterDescriptions { get; set; } = default!; - - #endregion } diff --git a/src/Nexus/Extensibility/DataSource/DataSourceController.cs b/src/Nexus/Extensibility/DataSource/DataSourceController.cs index c4c4c047..b5c86123 100644 --- a/src/Nexus/Extensibility/DataSource/DataSourceController.cs +++ b/src/Nexus/Extensibility/DataSource/DataSourceController.cs @@ -55,17 +55,11 @@ Task ReadAsync( internal class DataSourceController : IDataSourceController { - #region Fields - private readonly IProcessingService _processingService; private readonly ICacheService _cacheService; private readonly DataOptions _dataOptions; private ConcurrentDictionary _catalogCache = default!; - #endregion - - #region Constructors - public DataSourceController( IDataSource dataSource, InternalDataSourceRegistration registration, @@ -87,10 +81,6 @@ public DataSourceController( _dataOptions = dataOptions; } - #endregion - - #region Properties - private IDataSource DataSource { get; } private InternalDataSourceRegistration DataSourceRegistration { get; } @@ -101,10 +91,6 @@ public DataSourceController( private ILogger Logger { get; } - #endregion - - #region Methods - public async Task InitializeAsync( ConcurrentDictionary catalogCache, ILogger logger, @@ -796,10 +782,6 @@ private ReadUnit[] PrepareReadUnits( return readUnits.ToArray(); } - #endregion - - #region Static Methods - public static async Task ReadAsync( DateTime begin, DateTime end, @@ -1081,10 +1063,6 @@ private static void ValidateParameters( throw new ValidationException("The end parameter must be a multiple of the sample period."); } - #endregion - - #region IDisposable - private bool _disposedValue; protected virtual void Dispose(bool disposing) @@ -1106,6 +1084,4 @@ public void Dispose() Dispose(disposing: true); GC.SuppressFinalize(this); } - - #endregion } diff --git a/src/Nexus/Extensibility/DataSource/DataSourceDoubleStream.cs b/src/Nexus/Extensibility/DataSource/DataSourceDoubleStream.cs index f287b1b8..d9958d74 100644 --- a/src/Nexus/Extensibility/DataSource/DataSourceDoubleStream.cs +++ b/src/Nexus/Extensibility/DataSource/DataSourceDoubleStream.cs @@ -4,18 +4,12 @@ namespace Nexus.Extensibility; internal class DataSourceDoubleStream : Stream { - #region Fields - private readonly CancellationTokenSource _cts = new(); private long _position; private readonly long _length; private readonly PipeReader _reader; private readonly Stream _stream; - #endregion - - #region Constructors - public DataSourceDoubleStream(long length, PipeReader reader) { _length = length; @@ -23,10 +17,6 @@ public DataSourceDoubleStream(long length, PipeReader reader) _stream = reader.AsStream(); } - #endregion - - #region Properties - public override bool CanRead => true; public override bool CanSeek => false; @@ -47,10 +37,6 @@ public override long Position } } - #endregion - - #region Methods - public void Cancel() { _reader.CancelPendingRead(); @@ -118,6 +104,4 @@ protected override void Dispose(bool disposing) { _stream.Dispose(); } - - #endregion } diff --git a/src/Nexus/Extensibility/DataWriter/DataWriterController.cs b/src/Nexus/Extensibility/DataWriter/DataWriterController.cs index f516b551..201e8f88 100644 --- a/src/Nexus/Extensibility/DataWriter/DataWriterController.cs +++ b/src/Nexus/Extensibility/DataWriter/DataWriterController.cs @@ -26,8 +26,6 @@ Task WriteAsync( internal class DataWriterController : IDataWriterController { - #region Constructors - public DataWriterController( IDataWriter dataWriter, Uri resourceLocator, @@ -42,10 +40,6 @@ public DataWriterController( Logger = logger; } - #endregion - - #region Properties - private IReadOnlyDictionary? SystemConfiguration { get; } private IReadOnlyDictionary? RequestConfiguration { get; } @@ -56,10 +50,6 @@ public DataWriterController( private ILogger Logger { get; } - #endregion - - #region Methods - public async Task InitializeAsync( ILogger logger, CancellationToken cancellationToken) @@ -248,10 +238,6 @@ private static void ValidateParameters( throw new ValidationException("The file period parameter must be a multiple of the sample period."); } - #endregion - - #region IDisposable - private bool _disposedValue; protected virtual void Dispose(bool disposing) @@ -273,6 +259,4 @@ public void Dispose() Dispose(disposing: true); GC.SuppressFinalize(this); } - - #endregion } \ No newline at end of file diff --git a/src/Nexus/Extensions/Sources/Sample.cs b/src/Nexus/Extensions/Sources/Sample.cs index d8bd6217..332a47b8 100644 --- a/src/Nexus/Extensions/Sources/Sample.cs +++ b/src/Nexus/Extensions/Sources/Sample.cs @@ -10,19 +10,17 @@ namespace Nexus.Sources; "https://github.com/malstroem-labs/nexus/blob/master/src/Nexus/Extensions/Sources/Sample.cs")] internal class Sample : IDataSource { - #region Fields - public static Guid RegistrationId = new("c2c724ab-9002-4879-9cd9-2147844bee96"); - private static readonly double[] DATA = new double[] - { + private static readonly double[] DATA = + [ 6.5, 6.7, 7.9, 8.1, 7.5, 7.6, 7.0, 6.5, 6.0, 5.9, 5.8, 5.2, 4.6, 5.0, 5.1, 4.9, 5.3, 5.8, 5.9, 6.1, 5.9, 6.3, 6.5, 6.9, 7.1, 6.9, 7.1, 7.2, 7.6, 7.9, 8.2, 8.1, 8.2, 8.0, 7.5, 7.7, 7.6, 8.0, 7.5, 7.2, 6.8, 6.5, 6.6, 6.6, 6.7, 6.2, 5.9, 5.7, 5.9, 6.3, 6.6, 6.7, 6.9, 6.5, 6.0, 5.8, 5.3, 5.8, 6.1, 6.8 - }; + ]; public const string LocalCatalogId = "/SAMPLE/LOCAL"; public const string RemoteCatalogId = "/SAMPLE/REMOTE"; @@ -33,16 +31,8 @@ internal class Sample : IDataSource public const string RemoteUsername = "test"; public const string RemotePassword = "1234"; - #endregion - - #region Properties - private DataSourceContext Context { get; set; } = default!; - #endregion - - #region Methods - public Task SetContextAsync( DataSourceContext context, ILogger logger, @@ -229,6 +219,4 @@ private static double ToUnixTimeStamp( { return value.Subtract(new DateTime(1970, 1, 1)).TotalSeconds; } - - #endregion } diff --git a/src/Nexus/Extensions/Writers/Csv.cs b/src/Nexus/Extensions/Writers/Csv.cs index 30f77a9f..516e5be9 100644 --- a/src/Nexus/Extensions/Writers/Csv.cs +++ b/src/Nexus/Extensions/Writers/Csv.cs @@ -23,8 +23,6 @@ namespace Nexus.Writers; "https://github.com/malstroem-labs/nexus/blob/master/src/Nexus/Extensions/Writers/Csv.cs")] internal class Csv : IDataWriter, IDisposable { - #region "Fields" - private const string DESCRIPTION = """ { "label":"CSV + Schema (*.csv)", @@ -73,16 +71,8 @@ internal class Csv : IDataWriter, IDisposable private TimeSpan _lastSamplePeriod; private readonly Dictionary _resourceMap = new(); - #endregion - - #region Properties - private DataWriterContext Context { get; set; } = default!; - #endregion - - #region "Methods" - public Task SetContextAsync( DataWriterContext context, ILogger logger, @@ -328,10 +318,6 @@ private static string ToISO8601(DateTime dateTime) return dateTime.ToUniversalTime().ToString("yyyy-MM-ddTHH-mm-ssZ"); } - #endregion - - #region IDisposable - private bool _disposedValue; protected virtual void Dispose(bool disposing) @@ -356,6 +342,4 @@ public void Dispose() Dispose(disposing: true); GC.SuppressFinalize(this); } - - #endregion } diff --git a/src/Nexus/PackageManagement/PackageController.cs b/src/Nexus/PackageManagement/PackageController.cs index 129ef841..4ac1a7ef 100644 --- a/src/Nexus/PackageManagement/PackageController.cs +++ b/src/Nexus/PackageManagement/PackageController.cs @@ -14,8 +14,6 @@ namespace Nexus.PackageManagement; internal partial class PackageController { - #region Fields - public static Guid BUILTIN_ID = new("97d297d2-df6f-4c85-9d07-86bc64a041a6"); public const string BUILTIN_PROVIDER = "nexus"; @@ -27,26 +25,14 @@ internal partial class PackageController private readonly ILogger _logger; private PackageLoadContext? _loadContext; - #endregion - - #region Constructors - public PackageController(InternalPackageReference packageReference, ILogger logger) { PackageReference = packageReference; _logger = logger; } - #endregion - - #region Properties - public InternalPackageReference PackageReference { get; } - #endregion - - #region Methods - public async Task DiscoverAsync(CancellationToken cancellationToken) { _logger.LogDebug("Discover package versions using provider {Provider}", PackageReference.Provider); @@ -208,8 +194,6 @@ async Task GetAssetResponseAsync() } } - #endregion - #region local private Task DiscoverLocalAsync(CancellationToken cancellationToken) diff --git a/src/Nexus/PackageManagement/PackageLoadContext.cs b/src/Nexus/PackageManagement/PackageLoadContext.cs index d7bbc746..ec18b297 100644 --- a/src/Nexus/PackageManagement/PackageLoadContext.cs +++ b/src/Nexus/PackageManagement/PackageLoadContext.cs @@ -5,23 +5,13 @@ namespace Nexus.PackageManagement; internal class PackageLoadContext : AssemblyLoadContext { - #region Fields - private readonly AssemblyDependencyResolver _resolver; - #endregion - - #region Constructors - public PackageLoadContext(string entryDllPath) : base(isCollectible: true) { _resolver = new AssemblyDependencyResolver(entryDllPath); } - #endregion - - #region Methods - protected override Assembly? Load(AssemblyName assemblyName) { var assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName); @@ -41,6 +31,4 @@ protected override IntPtr LoadUnmanagedDll(string unmanagedDllName) return IntPtr.Zero; } - - #endregion } diff --git a/src/Nexus/Services/AppStateManager.cs b/src/Nexus/Services/AppStateManager.cs index 17ac62d1..2823a0f5 100644 --- a/src/Nexus/Services/AppStateManager.cs +++ b/src/Nexus/Services/AppStateManager.cs @@ -9,8 +9,6 @@ namespace Nexus.Services; internal class AppStateManager { - #region Fields - private readonly IExtensionHive _extensionHive; private readonly ICatalogManager _catalogManager; private readonly IDatabaseService _databaseService; @@ -18,10 +16,6 @@ internal class AppStateManager private readonly SemaphoreSlim _refreshDatabaseSemaphore = new(initialCount: 1, maxCount: 1); private readonly SemaphoreSlim _projectSemaphore = new(initialCount: 1, maxCount: 1); - #endregion - - #region Constructors - public AppStateManager( AppState appState, IExtensionHive extensionHive, @@ -36,16 +30,8 @@ public AppStateManager( _logger = logger; } - #endregion - - #region Properties - public AppState AppState { get; } - #endregion - - #region Methods - public async Task RefreshDatabaseAsync( IProgress progress, CancellationToken cancellationToken) @@ -308,6 +294,4 @@ private async Task SaveProjectAsync(NexusProject project) using var stream = _databaseService.WriteProject(); await JsonSerializerHelper.SerializeIndentedAsync(stream, project); } - - #endregion } diff --git a/src/Nexus/Services/CatalogManager.cs b/src/Nexus/Services/CatalogManager.cs index 2da4aa5f..f4031714 100644 --- a/src/Nexus/Services/CatalogManager.cs +++ b/src/Nexus/Services/CatalogManager.cs @@ -18,8 +18,6 @@ Task GetCatalogContainersAsync( internal class CatalogManager : ICatalogManager { - #region Types - record CatalogPrototype( CatalogRegistration Registration, InternalDataSourceRegistration DataSourceRegistration, @@ -27,10 +25,6 @@ record CatalogPrototype( CatalogMetadata Metadata, ClaimsPrincipal? Owner); - #endregion - - #region Fields - private readonly AppState _appState; private readonly IDataControllerService _dataControllerService; private readonly IDatabaseService _databaseService; @@ -38,10 +32,6 @@ record CatalogPrototype( private readonly IExtensionHive _extensionHive; private readonly ILogger _logger; - #endregion - - #region Constructors - public CatalogManager( AppState appState, IDataControllerService dataControllerService, @@ -58,10 +48,6 @@ public CatalogManager( _logger = logger; } - #endregion - - #region Methods - public async Task GetCatalogContainersAsync( CatalogContainer parent, CancellationToken cancellationToken) @@ -317,6 +303,4 @@ private CatalogPrototype[] EnsureNoHierarchy( return catalogPrototypesToKeep.ToArray(); } - - #endregion } diff --git a/src/Nexus/Services/DataService.cs b/src/Nexus/Services/DataService.cs index 4d90c1d8..15cc2dc9 100644 --- a/src/Nexus/Services/DataService.cs +++ b/src/Nexus/Services/DataService.cs @@ -36,8 +36,6 @@ Task ExportAsync( internal class DataService : IDataService { - #region Fields - private readonly AppState _appState; private readonly IMemoryTracker _memoryTracker; private readonly ClaimsPrincipal _user; @@ -46,10 +44,6 @@ internal class DataService : IDataService private readonly IDatabaseService _databaseService; private readonly IDataControllerService _dataControllerService; - #endregion - - #region Constructors - public DataService( AppState appState, ClaimsPrincipal user, @@ -71,18 +65,10 @@ public DataService( WriteProgress = new Progress(); } - #endregion - - #region Properties - public Progress ReadProgress { get; } public Progress WriteProgress { get; } - #endregion - - #region Methods - public async Task ReadAsStreamAsync( string resourcePath, DateTime begin, @@ -395,6 +381,4 @@ private static void CleanUp(string directoryPath) // } } - - #endregion } diff --git a/src/Nexus/Services/ExtensionHive.cs b/src/Nexus/Services/ExtensionHive.cs index 4ce42eea..0147fcdd 100644 --- a/src/Nexus/Services/ExtensionHive.cs +++ b/src/Nexus/Services/ExtensionHive.cs @@ -31,18 +31,12 @@ Task GetVersionsAsync( internal class ExtensionHive : IExtensionHive { - #region Fields - private readonly ILogger _logger; private readonly ILoggerFactory _loggerFactory; private readonly PathsOptions _pathsOptions; private Dictionary>? _packageControllerMap = default!; - #endregion - - #region Constructors - public ExtensionHive( IOptions pathsOptions, ILogger logger, @@ -53,10 +47,6 @@ public ExtensionHive( _pathsOptions = pathsOptions.Value; } - #endregion - - #region Methods - public async Task LoadPackagesAsync( IEnumerable packageReferences, IProgress progress, @@ -221,6 +211,4 @@ private ReadOnlyCollection ScanAssembly(Assembly assembly, IEnumerable _jobs = new(); - #endregion - - #region Constructors - public JobService() { _timer = new Timer() @@ -58,10 +52,6 @@ public JobService() }; } - #endregion - - #region Methods - public JobControl AddJob( Job job, Progress progress, @@ -119,6 +109,4 @@ public List GetJobs() .Select(entry => entry.Value) .ToList(); } - - #endregion } diff --git a/src/extensibility/dotnet-extensibility/DataModel/Representation.cs b/src/extensibility/dotnet-extensibility/DataModel/Representation.cs index b2c6a2be..7dd6ebfa 100644 --- a/src/extensibility/dotnet-extensibility/DataModel/Representation.cs +++ b/src/extensibility/dotnet-extensibility/DataModel/Representation.cs @@ -11,17 +11,11 @@ namespace Nexus.DataModel; [DebuggerDisplay("{Id,nq}")] public record Representation { - #region Fields - private static readonly Regex _snakeCaseEvaluator = new("(?<=[a-z])([A-Z])", RegexOptions.Compiled); private static readonly HashSet _nexusDataTypeValues = new(Enum.GetValues()); private IReadOnlyDictionary? _parameters; - #endregion - - #region Constructors - /// /// Initializes a new instance of the . /// @@ -75,10 +69,6 @@ internal Representation( } } - #endregion - - #region Properties - /// /// The identifer of the representation. It is constructed using the sample period. /// @@ -127,10 +117,6 @@ public IReadOnlyDictionary? Parameters [JsonIgnore] public int ElementSize => ((int)DataType & 0xFF) >> 3; - #endregion - - #region "Methods" - internal Representation DeepCopy() { return new Representation( @@ -150,6 +136,4 @@ private static void ValidateParameters(IReadOnlyDictionary throw new Exception("The representation argument identifier is not valid."); } } - - #endregion } \ No newline at end of file diff --git a/src/extensibility/dotnet-extensibility/DataModel/Resource.cs b/src/extensibility/dotnet-extensibility/DataModel/Resource.cs index 48f31182..29bf620a 100644 --- a/src/extensibility/dotnet-extensibility/DataModel/Resource.cs +++ b/src/extensibility/dotnet-extensibility/DataModel/Resource.cs @@ -11,15 +11,9 @@ namespace Nexus.DataModel; [DebuggerDisplay("{Id,nq}")] public record Resource { - #region Fields - private string _id = default!; private IReadOnlyList? _representations; - #endregion - - #region Constructors - /// /// Initializes a new instance of the . /// @@ -37,10 +31,6 @@ public Resource( Representations = representations; } - #endregion - - #region Properties - /// /// Gets a regular expression to validate a resource identifier. /// @@ -105,10 +95,6 @@ public IReadOnlyList? Representations } } - #endregion - - #region "Methods" - internal Resource Merge(Resource resource) { if (Id != resource.Id) @@ -143,6 +129,4 @@ private static void ValidateRepresentations(IReadOnlyList repres if (uniqueIds.Count() != representations.Count) throw new ArgumentException("There are multiple representations with the same identifier."); } - - #endregion } \ No newline at end of file diff --git a/src/extensibility/dotnet-extensibility/DataModel/ResourceBuilder.cs b/src/extensibility/dotnet-extensibility/DataModel/ResourceBuilder.cs index c67027cc..ac1fdaa4 100644 --- a/src/extensibility/dotnet-extensibility/DataModel/ResourceBuilder.cs +++ b/src/extensibility/dotnet-extensibility/DataModel/ResourceBuilder.cs @@ -9,16 +9,10 @@ namespace Nexus.DataModel; [DebuggerDisplay("{Id,nq}")] public record ResourceBuilder { - #region Fields - private readonly string _id; private Dictionary? _properties; private List? _representations; - #endregion - - #region Constructors - /// /// Initializes a new instance of the . /// @@ -28,10 +22,6 @@ public ResourceBuilder(string id) _id = id; } - #endregion - - #region "Methods" - /// /// Adds a property. /// @@ -93,6 +83,4 @@ public Resource Build() { return new Resource(_id, _properties, _representations); } - - #endregion } \ No newline at end of file diff --git a/src/extensibility/dotnet-extensibility/DataModel/ResourceCatalog.cs b/src/extensibility/dotnet-extensibility/DataModel/ResourceCatalog.cs index 351981ff..941c6667 100644 --- a/src/extensibility/dotnet-extensibility/DataModel/ResourceCatalog.cs +++ b/src/extensibility/dotnet-extensibility/DataModel/ResourceCatalog.cs @@ -13,15 +13,9 @@ namespace Nexus.DataModel; [DebuggerDisplay("{Id,nq}")] public record ResourceCatalog { - #region Fields - private string _id = default!; private IReadOnlyList? _resources; - #endregion - - #region Constructors - /// /// Initializes a new instance of the . /// @@ -39,10 +33,6 @@ public ResourceCatalog( Resources = resources; } - #endregion - - #region Properties - /// /// Gets a regular expression to validate a resource catalog identifier. /// @@ -97,10 +87,6 @@ public IReadOnlyList? Resources } } - #endregion - - #region "Methods" - /// /// Merges another catalog with this instance. /// @@ -216,6 +202,4 @@ private static void ValidateResources(IReadOnlyList resources) if (uniqueIds.Count() != resources.Count) throw new ArgumentException("There are multiple resources with the same identifier."); } - - #endregion } diff --git a/src/extensibility/dotnet-extensibility/DataModel/ResourceCatalogBuilder.cs b/src/extensibility/dotnet-extensibility/DataModel/ResourceCatalogBuilder.cs index 8bb21878..0a7b588c 100644 --- a/src/extensibility/dotnet-extensibility/DataModel/ResourceCatalogBuilder.cs +++ b/src/extensibility/dotnet-extensibility/DataModel/ResourceCatalogBuilder.cs @@ -7,16 +7,10 @@ namespace Nexus.DataModel; /// public record ResourceCatalogBuilder { - #region Fields - private readonly string _id; private Dictionary? _properties; private List? _resources; - #endregion - - #region Constructors - /// /// Initializes a new instance of the . /// @@ -26,10 +20,6 @@ public ResourceCatalogBuilder(string id) _id = id; } - #endregion - - #region "Methods" - /// /// Adds a property. /// @@ -106,6 +96,4 @@ public ResourceCatalog Build() { return new ResourceCatalog(_id, _properties, _resources); } - - #endregion } diff --git a/src/extensibility/dotnet-extensibility/Extensibility/DataSource/SimpleDataSource.cs b/src/extensibility/dotnet-extensibility/Extensibility/DataSource/SimpleDataSource.cs index bc02f886..b6813ec0 100644 --- a/src/extensibility/dotnet-extensibility/Extensibility/DataSource/SimpleDataSource.cs +++ b/src/extensibility/dotnet-extensibility/Extensibility/DataSource/SimpleDataSource.cs @@ -8,8 +8,6 @@ namespace Nexus.Extensibility; /// public abstract class SimpleDataSource : IDataSource { - #region Properties - /// /// Gets the data source context. This property is not accessible from within class constructors as it will bet set later. /// @@ -20,10 +18,6 @@ public abstract class SimpleDataSource : IDataSource /// protected ILogger Logger { get; private set; } = default!; - #endregion - - #region Methods - /// public Task SetContextAsync( DataSourceContext context, @@ -72,6 +66,4 @@ public abstract Task ReadAsync( ReadDataHandler readData, IProgress progress, CancellationToken cancellationToken); - - #endregion } diff --git a/tests/Nexus.Tests/Other/PackageControllerTests.cs b/tests/Nexus.Tests/Other/PackageControllerTests.cs index d6e87c57..473324bf 100644 --- a/tests/Nexus.Tests/Other/PackageControllerTests.cs +++ b/tests/Nexus.Tests/Other/PackageControllerTests.cs @@ -12,8 +12,6 @@ namespace Other; public class PackageControllerTests { - #region Constants - // Need to do it this way because GitHub revokes obvious tokens on commit. // However, this token - in combination with the test user's account // privileges - allows only read-only access to a test project, so there @@ -115,8 +113,6 @@ public class PackageControllerTests 0x41 ]; - #endregion - #region Load [Fact] From 0ef2d8143bea64d283b2b7e9190b9377a181343c Mon Sep 17 00:00:00 2001 From: Apollo3zehn Date: Thu, 7 Mar 2024 21:50:41 +0100 Subject: [PATCH 4/5] Prefer var --- .editorconfig | 13 +++- .../Components/Leftbar_ChartSettings.razor | 7 +- .../Components/UserSettingsView.razor | 2 +- src/Nexus/Extensions/Sources/Sample.cs | 66 +++++++++++++++++-- .../PackageManagement/PackageController.cs | 8 ++- src/Nexus/Services/DataService.cs | 4 +- src/Nexus/Services/ProcessingService.cs | 4 +- src/Nexus/Utilities/NexusUtilities.cs | 4 +- .../DataModel/DataModelUtilities.cs | 2 +- .../DataSource/DataSourceControllerTests.cs | 12 ++-- 10 files changed, 91 insertions(+), 31 deletions(-) diff --git a/.editorconfig b/.editorconfig index 05cea914..2fbcf6b6 100755 --- a/.editorconfig +++ b/.editorconfig @@ -2,7 +2,7 @@ # Run dotnet format --diagnostics XXXX --severity info # How to apply all rules: -# Run dotnet format --severity info/warning/error +# Run dotnet format --severity error/info/warn/ [*] trim_trailing_whitespace = true @@ -11,12 +11,19 @@ trim_trailing_whitespace = true # "run cleanup": https://betterprogramming.pub/enforce-net-code-style-with-editorconfig-d2f0d79091ac # TODO: build real editorconfig file: https://github.com/dotnet/roslyn/blob/main/.editorconfig -# Use file scoped namespace declarations -csharp_style_namespace_declarations = file_scoped:warning +# Prefer var +csharp_style_var_for_built_in_types = false +csharp_style_var_when_type_is_apparent = true +csharp_style_var_elsewhere = true +dotnet_diagnostic.IDE0007.severity = warning # Make field dotnet_diagnostic.IDE0044.severity = warning +# Use file scoped namespace declarations +dotnet_diagnostic.IDE0161.severity = error +csharp_style_namespace_declarations = file_scoped + # Enable naming rule violation errors on build (alternative: dotnet_analyzer_diagnostic.category-Style.severity = error) dotnet_diagnostic.IDE1006.severity = error diff --git a/src/Nexus.UI/Components/Leftbar_ChartSettings.razor b/src/Nexus.UI/Components/Leftbar_ChartSettings.razor index 33cadac5..92a41130 100644 --- a/src/Nexus.UI/Components/Leftbar_ChartSettings.razor +++ b/src/Nexus.UI/Components/Leftbar_ChartSettings.razor @@ -4,11 +4,8 @@ @if (AppState.ViewState == ViewState.Data) {
- -
+ + } @code { diff --git a/src/Nexus.UI/Components/UserSettingsView.razor b/src/Nexus.UI/Components/UserSettingsView.razor index dea7baff..313c65e9 100644 --- a/src/Nexus.UI/Components/UserSettingsView.razor +++ b/src/Nexus.UI/Components/UserSettingsView.razor @@ -124,7 +124,7 @@
- @for (var i = 0; i < _newAccessTokenClaims.Count; i++) + @for (int i = 0; i < _newAccessTokenClaims.Count; i++) { var local = i; diff --git a/src/Nexus/Extensions/Sources/Sample.cs b/src/Nexus/Extensions/Sources/Sample.cs index 332a47b8..93624630 100644 --- a/src/Nexus/Extensions/Sources/Sample.cs +++ b/src/Nexus/Extensions/Sources/Sample.cs @@ -14,12 +14,66 @@ internal class Sample : IDataSource private static readonly double[] DATA = [ - 6.5, 6.7, 7.9, 8.1, 7.5, 7.6, 7.0, 6.5, 6.0, 5.9, - 5.8, 5.2, 4.6, 5.0, 5.1, 4.9, 5.3, 5.8, 5.9, 6.1, - 5.9, 6.3, 6.5, 6.9, 7.1, 6.9, 7.1, 7.2, 7.6, 7.9, - 8.2, 8.1, 8.2, 8.0, 7.5, 7.7, 7.6, 8.0, 7.5, 7.2, - 6.8, 6.5, 6.6, 6.6, 6.7, 6.2, 5.9, 5.7, 5.9, 6.3, - 6.6, 6.7, 6.9, 6.5, 6.0, 5.8, 5.3, 5.8, 6.1, 6.8 + 6.5, + 6.7, + 7.9, + 8.1, + 7.5, + 7.6, + 7.0, + 6.5, + 6.0, + 5.9, + 5.8, + 5.2, + 4.6, + 5.0, + 5.1, + 4.9, + 5.3, + 5.8, + 5.9, + 6.1, + 5.9, + 6.3, + 6.5, + 6.9, + 7.1, + 6.9, + 7.1, + 7.2, + 7.6, + 7.9, + 8.2, + 8.1, + 8.2, + 8.0, + 7.5, + 7.7, + 7.6, + 8.0, + 7.5, + 7.2, + 6.8, + 6.5, + 6.6, + 6.6, + 6.7, + 6.2, + 5.9, + 5.7, + 5.9, + 6.3, + 6.6, + 6.7, + 6.9, + 6.5, + 6.0, + 5.8, + 5.3, + 5.8, + 6.1, + 6.8 ]; public const string LocalCatalogId = "/SAMPLE/LOCAL"; diff --git a/src/Nexus/PackageManagement/PackageController.cs b/src/Nexus/PackageManagement/PackageController.cs index 4ac1a7ef..cb550618 100644 --- a/src/Nexus/PackageManagement/PackageController.cs +++ b/src/Nexus/PackageManagement/PackageController.cs @@ -36,9 +36,10 @@ public PackageController(InternalPackageReference packageReference, ILogger DiscoverAsync(CancellationToken cancellationToken) { _logger.LogDebug("Discover package versions using provider {Provider}", PackageReference.Provider); - string[] result = PackageReference.Provider switch + + var result = PackageReference.Provider switch { - BUILTIN_PROVIDER => new string[] { "current" }, + BUILTIN_PROVIDER => ["current"], "local" => await DiscoverLocalAsync(cancellationToken), "git-tag" => await DiscoverGitTagsAsync(cancellationToken), "github-releases" => await DiscoverGithubReleasesAsync(cancellationToken), @@ -48,6 +49,7 @@ public async Task DiscoverAsync(CancellationToken cancellationToken) */ _ => throw new ArgumentException($"The provider {PackageReference.Provider} is not supported."), }; + return result; } @@ -107,7 +109,7 @@ internal async Task RestoreAsync(string restoreRoot, CancellationToken c var actualRestoreRoot = Path.Combine(restoreRoot, PackageReference.Provider); _logger.LogDebug("Restore package to {RestoreRoot} using provider {Provider}", actualRestoreRoot, PackageReference.Provider); - string restoreFolderPath = PackageReference.Provider switch + var restoreFolderPath = PackageReference.Provider switch { "local" => await RestoreLocalAsync(actualRestoreRoot, cancellationToken), "git-tag" => await RestoreGitTagAsync(actualRestoreRoot, cancellationToken), diff --git a/src/Nexus/Services/DataService.cs b/src/Nexus/Services/DataService.cs index 15cc2dc9..e76ede98 100644 --- a/src/Nexus/Services/DataService.cs +++ b/src/Nexus/Services/DataService.cs @@ -163,7 +163,7 @@ public async Task ExportAsync( throw new ValidationException("The file period must be a multiple of the sample period."); // start - string zipFileName = string.Empty; + var zipFileName = string.Empty; IDataWriterController? controller = default!; var tmpFolderPath = Path.Combine(Path.GetTempPath(), "Nexus", Guid.NewGuid().ToString()); @@ -347,7 +347,7 @@ private void WriteZipArchiveEntries(ZipArchive zipArchive, string sourceFolderPa var fileCount = filePaths.Length; var currentCount = 0; - foreach (string filePath in filePaths) + foreach (var filePath in filePaths) { cancellationToken.ThrowIfCancellationRequested(); diff --git a/src/Nexus/Services/ProcessingService.cs b/src/Nexus/Services/ProcessingService.cs index ea5fcd91..54ccf4c8 100644 --- a/src/Nexus/Services/ProcessingService.cs +++ b/src/Nexus/Services/ProcessingService.cs @@ -288,7 +288,7 @@ private void ApplyAggregationFunction( { case RepresentationKind.MinBitwise: - T[] bitField_and = new T[targetBuffer.Length]; + var bitField_and = new T[targetBuffer.Length]; Parallel.For(0, targetBuffer.Length, x => { @@ -324,7 +324,7 @@ private void ApplyAggregationFunction( case RepresentationKind.MaxBitwise: - T[] bitField_or = new T[targetBuffer.Length]; + var bitField_or = new T[targetBuffer.Length]; Parallel.For(0, targetBuffer.Length, x => { diff --git a/src/Nexus/Utilities/NexusUtilities.cs b/src/Nexus/Utilities/NexusUtilities.cs index f9ca3b9e..729de5ea 100644 --- a/src/Nexus/Utilities/NexusUtilities.cs +++ b/src/Nexus/Utilities/NexusUtilities.cs @@ -16,7 +16,7 @@ public static string DefaultBaseUrl { if (_defaultBaseUrl is null) { - int port = 5000; + var port = 5000; var aspnetcoreEnvVar = Environment.GetEnvironmentVariable("ASPNETCORE_URLS"); if (aspnetcoreEnvVar is not null) @@ -85,7 +85,7 @@ public static async ValueTask WhenAll(params ValueTask[] tasks) var results = new T[tasks.Length]; - for (var i = 0; i < tasks.Length; i++) + for (int i = 0; i < tasks.Length; i++) { try { diff --git a/src/extensibility/dotnet-extensibility/DataModel/DataModelUtilities.cs b/src/extensibility/dotnet-extensibility/DataModel/DataModelUtilities.cs index 9322846e..bbcf8de7 100644 --- a/src/extensibility/dotnet-extensibility/DataModel/DataModelUtilities.cs +++ b/src/extensibility/dotnet-extensibility/DataModel/DataModelUtilities.cs @@ -222,7 +222,7 @@ private static void MergeObjects(JsonObject currentObject, JsonElement root1, Js { foreach (var property in root1.EnumerateObject()) { - if (root2.TryGetProperty(property.Name, out JsonElement newValue) && newValue.ValueKind != JsonValueKind.Null) + if (root2.TryGetProperty(property.Name, out var newValue) && newValue.ValueKind != JsonValueKind.Null) { var originalValue = property.Value; var originalValueKind = originalValue.ValueKind; diff --git a/tests/Nexus.Tests/DataSource/DataSourceControllerTests.cs b/tests/Nexus.Tests/DataSource/DataSourceControllerTests.cs index 51d841cf..d05e5347 100644 --- a/tests/Nexus.Tests/DataSource/DataSourceControllerTests.cs +++ b/tests/Nexus.Tests/DataSource/DataSourceControllerTests.cs @@ -141,11 +141,11 @@ public async Task CanRead() new DataReadingGroup(controller, catalogItemRequestPipeWriters) }; - double[] result1 = new double[86401]; + var result1 = new double[86401]; var writing1 = Task.Run(async () => { - Memory resultBuffer1 = result1.AsMemory().Cast(); + var resultBuffer1 = result1.AsMemory().Cast(); var stream1 = pipe1.Reader.AsStream(); while (resultBuffer1.Length > 0) @@ -160,11 +160,11 @@ public async Task CanRead() } }); - double[] result2 = new double[86401]; + var result2 = new double[86401]; var writing2 = Task.Run(async () => { - Memory resultBuffer2 = result2.AsMemory().Cast(); + var resultBuffer2 = result2.AsMemory().Cast(); var stream2 = pipe2.Reader.AsStream(); while (resultBuffer2.Length > 0) @@ -249,11 +249,11 @@ public async Task CanReadAsStream() NullLogger.Instance, CancellationToken.None); - double[] result = new double[86401]; + var result = new double[86401]; await Task.Run(async () => { - Memory resultBuffer = result.AsMemory().Cast(); + var resultBuffer = result.AsMemory().Cast(); while (resultBuffer.Length > 0) { From 8e637992d2c8ba96fafb33d0423feb32a91e1dc9 Mon Sep 17 00:00:00 2001 From: Apollo3zehn Date: Thu, 7 Mar 2024 21:56:25 +0100 Subject: [PATCH 5/5] Add .venv to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 4339ec80..4bbc1fa1 100755 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .vs/ .venv/ + artifacts/ BenchmarkDotNet.Artifacts