From ff016f8f1aec14f53cb5c86387ef238b61825755 Mon Sep 17 00:00:00 2001 From: Apollo3zehn Date: Thu, 7 Mar 2024 19:04:32 +0100 Subject: [PATCH 01/19] Run CI on pull request --- .github/workflows/build-and-publish.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/build-and-publish.yml b/.github/workflows/build-and-publish.yml index 73d666af..7a4da0d1 100644 --- a/.github/workflows/build-and-publish.yml +++ b/.github/workflows/build-and-publish.yml @@ -9,6 +9,10 @@ on: tags: - '*' + pull_request: + branches: + - dev + jobs: build: From 2422872e12695bdb77d9ae02166a09d8dde78d27 Mon Sep 17 00:00:00 2001 From: Apollo3zehn Date: Thu, 7 Mar 2024 19:05:00 +0100 Subject: [PATCH 02/19] Prepare replacement of JWT with simple file system stored personal access tokens --- src/Nexus/API/UsersController.cs | 111 ++++------ src/Nexus/Core/Models_NonPublic.cs | 30 +-- src/Nexus/Core/Models_Public.cs | 100 +++------ src/Nexus/Core/NexusAuthExtensions.cs | 19 -- src/Nexus/Core/NexusOptions.cs | 4 - src/Nexus/Core/UserDbContext.cs | 10 - src/Nexus/Nexus.csproj | 8 - src/Nexus/Program.cs | 9 +- src/Nexus/Services/DatabaseService.cs | 55 ++++- src/Nexus/Services/DbService.cs | 40 +--- .../Services/NexusAuthenticationService.cs | 190 ------------------ src/Nexus/Services/TokenService.cs | 95 +++++++++ .../NexusAuthenticationServiceTests.cs | 8 +- 13 files changed, 224 insertions(+), 455 deletions(-) delete mode 100644 src/Nexus/Services/NexusAuthenticationService.cs create mode 100644 src/Nexus/Services/TokenService.cs diff --git a/src/Nexus/API/UsersController.cs b/src/Nexus/API/UsersController.cs index d0004b3b..0e593b30 100644 --- a/src/Nexus/API/UsersController.cs +++ b/src/Nexus/API/UsersController.cs @@ -27,13 +27,12 @@ internal class UsersController : ControllerBase // GET /api/users/authentication-schemes // GET /api/users/authenticate // GET /api/users/signout - // POST /api/users/tokens/refresh - // POST /api/users/tokens/revoke + // POST /api/users/tokens/delete // [authenticated] // GET /api/users/me // GET /api/users/accept-license?catalogId=X - // POST /api/users/tokens/generate + // POST /api/users/tokens/create // DELETE /api/users/tokens/{tokenId} // [privileged] @@ -50,7 +49,7 @@ internal class UsersController : ControllerBase #region Fields private readonly IDBService _dbService; - private readonly INexusAuthenticationService _authService; + private readonly ITokenService _tokenService; private readonly SecurityOptions _securityOptions; private readonly ILogger _logger; @@ -60,12 +59,12 @@ internal class UsersController : ControllerBase public UsersController( IDBService dBService, - INexusAuthenticationService authService, + ITokenService tokenService, IOptions securityOptions, ILogger logger) { _dbService = dBService; - _authService = authService; + _tokenService = tokenService; _securityOptions = securityOptions.Value; _logger = logger; } @@ -127,58 +126,24 @@ public async Task SignOutAsync( } /// - /// Refreshes the JWT token. + /// Deletes a personal access token. /// - /// The refresh token request. - /// A new pair of JWT and refresh token. + /// The personal access token to delete. [AllowAnonymous] - [HttpPost("tokens/refresh")] - public async Task> RefreshTokenAsync(RefreshTokenRequest request) + [HttpDelete("tokens/delete")] + public async Task DeleteTokenByValueAsync( + [BindRequired] string value) { // get token - var internalRefreshToken = InternalRefreshToken.Deserialize(request.RefreshToken); - var token = await _dbService.FindRefreshTokenAsync(internalRefreshToken.Id, includeUserClaims: true); + // var internalRefreshToken = InternalRefreshToken.Deserialize(); + // var token = await _dbService.FindRefreshTokenAsync(internalRefreshToken.Id, includeUserClaims: false); - if (token is null) - return NotFound("Token not found."); + // if (token is null) + // return NotFound("Token not found."); - // check token - if (token.Token != request.RefreshToken) - { - _logger.LogWarning($"Attempted reuse of revoked token of user {token.Owner.Id} ({token.Owner.Name})."); - - # warning Temporarily disabled - // await _authService.RevokeTokenAsync(token); - } - - if (token.IsExpired) - return UnprocessableEntity("Invalid token."); - - // refresh token - var tokenPair = await _authService - .RefreshTokenAsync(token); - - return tokenPair; - } - - /// - /// Revokes a refresh token. - /// - /// The revoke token request. - [AllowAnonymous] - [HttpPost("tokens/revoke")] - public async Task RevokeTokenAsync(RevokeTokenRequest request) - { - // get token - var internalRefreshToken = InternalRefreshToken.Deserialize(request.RefreshToken); - var token = await _dbService.FindRefreshTokenAsync(internalRefreshToken.Id, includeUserClaims: false); - - if (token is null) - return NotFound("Token not found."); - - // revoke token - await _authService - .RevokeTokenAsync(token); + // // revoke token + // await _authService + // .RevokeTokenAsync(token); return Ok(); } @@ -207,18 +172,18 @@ public async Task> GetMeAsync() user.Id, user, isAdmin, - user.RefreshTokens.ToDictionary(entry => entry.Id, entry => entry)); + default!); // user.RefreshTokens.ToDictionary(entry => entry.Id, entry => entry)); } /// - /// Generates a refresh token. + /// Creates a personal access token. /// - /// The refresh token description. + /// The create token request. /// The optional user identifier. If not specified, the current user will be used. [Authorize(AuthenticationSchemes = CookieAuthenticationDefaults.AuthenticationScheme)] - [HttpPost("tokens/generate")] - public async Task> GenerateRefreshTokenAsync( - [BindRequired] string description, + [HttpPost("tokens/create")] + public async Task> CreateTokenAsync( + CreateTokenRequest request, [FromQuery] string? userId = default ) { @@ -229,9 +194,10 @@ public async Task> GenerateRefreshTokenAsync( if (user is null) return NotFound($"Could not find user {userId}."); - var refreshToken = await _authService.GenerateRefreshTokenAsync(user, description); + // var refreshToken = await _authService.GenerateRefreshTokenAsync(user, description); - return Ok(refreshToken); + // return Ok(refreshToken); + throw new Exception(); } else @@ -279,23 +245,23 @@ public async Task AcceptLicenseAsync( } /// - /// Deletes a refresh token. + /// Deletes a personal access token. /// - /// The identifier of the refresh token. + /// The identifier of the personal access token. [HttpDelete("tokens/{tokenId}")] - public async Task DeleteRefreshTokenAsync( + public async Task DeleteTokenAsync( Guid tokenId) { - // TODO: Is this thread safe? Maybe yes, because of scoped EF context. + // // TODO: Is this thread safe? Maybe yes, because of scoped EF context. - var token = await _dbService.FindRefreshTokenAsync(tokenId, includeUserClaims: true); + // var token = await _dbService.FindRefreshTokenAsync(tokenId, includeUserClaims: true); - if (token is null) - return NotFound($"Could not find refresh token {tokenId}."); + // if (token is null) + // return NotFound($"Could not find refresh token {tokenId}."); - token.Owner.RefreshTokens.Remove(token); + // token.Owner.RefreshTokens.Remove(token); - await _dbService.SaveChangesAsync(); + // await _dbService.SaveChangesAsync(); return Ok(); } @@ -418,12 +384,12 @@ public async Task DeleteClaimAsync( } /// - /// Gets all refresh tokens. + /// Gets all personal access tokens. /// /// The identifier of the user. [Authorize(Policy = NexusPolicies.RequireAdmin)] [HttpGet("{userId}/tokens")] - public async Task>> GetRefreshTokensAsync( + public async Task>> GetPersonalAccessTokensAsync( string userId) { var user = await _dbService.FindUserAsync(userId); @@ -431,7 +397,8 @@ public async Task>> GetRefr if (user is null) return NotFound($"Could not find user {userId}."); - return Ok(user.RefreshTokens.ToDictionary(token => token.Id, token => token)); + // return Ok(user.RefreshTokens.ToDictionary(token => token.Id, token => token)); + throw new Exception(); } private bool TryAuthenticate( diff --git a/src/Nexus/Core/Models_NonPublic.cs b/src/Nexus/Core/Models_NonPublic.cs index d8f84c27..b46fab19 100644 --- a/src/Nexus/Core/Models_NonPublic.cs +++ b/src/Nexus/Core/Models_NonPublic.cs @@ -2,32 +2,16 @@ using Nexus.Extensibility; using System.IO.Pipelines; using System.Text.Json; -using System.Text.Json.Serialization; namespace Nexus.Core { - internal record InternalRefreshToken( - [property: JsonPropertyName("v")] int Version, - [property: JsonPropertyName("i")] Guid Id, - [property: JsonPropertyName("t")] string Value) - { - internal static string Serialize(InternalRefreshToken token) - { - var bytes = JsonSerializer.SerializeToUtf8Bytes(token); - var base64String = Convert.ToBase64String(bytes); - - return base64String; - } - - internal static InternalRefreshToken Deserialize(string token) - { - var bytes = Convert.FromBase64String(token); - var internalRefreshToken = JsonSerializer.Deserialize(bytes)!; - - return internalRefreshToken; - } - } - + internal record InternalPersonalAccessToken ( + Guid Id, + byte[] Secret, + string Description, + DateTime Expires, + IDictionary Claims); + internal record struct Interval( DateTime Begin, DateTime End); diff --git a/src/Nexus/Core/Models_Public.cs b/src/Nexus/Core/Models_Public.cs index 572e05a2..4435b860 100644 --- a/src/Nexus/Core/Models_Public.cs +++ b/src/Nexus/Core/Models_Public.cs @@ -1,6 +1,5 @@ using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Nexus.DataModel; -using System.ComponentModel.DataAnnotations; using System.Text.Json; using System.Text.Json.Serialization; @@ -20,7 +19,6 @@ public NexusUser( Id = id; Name = name; - RefreshTokens = new(); Claims = new(); } @@ -37,9 +35,6 @@ public NexusUser( #pragma warning disable CS1591 - [JsonIgnore] - public List RefreshTokens { get; set; } = default!; - [JsonIgnore] public List Claims { get; set; } = default!; @@ -88,80 +83,31 @@ public NexusClaim(Guid id, string type, string value) } /// - /// A refresh token. - /// - public class RefreshToken - { -#pragma warning disable CS1591 - - public RefreshToken(Guid id, string token, DateTime expires, string description) - { - Id = id; - Token = token; - Expires = expires; - Description = description; - - InternalRefreshToken = InternalRefreshToken.Deserialize(token); - } - - [JsonIgnore] - [ValidateNever] - public Guid Id { get; init; } - - [JsonIgnore] - public string Token { get; init; } - -#pragma warning restore CS1591 - - /// - /// The date/time when the token expires. - /// - public DateTime Expires { get; init; } - - /// - /// The token description. - /// - public string Description { get; init; } - - [JsonIgnore] - #warning Remove this when https://github.com/RicoSuter/NSwag/issues/4681 is solved - internal bool IsExpired => DateTime.UtcNow >= Expires; - - [JsonIgnore] - #warning Remove this when https://github.com/RicoSuter/NSwag/issues/4681 is solved - internal InternalRefreshToken InternalRefreshToken { get; } - -#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] - public NexusUser Owner { get; set; } = default!; - -#pragma warning restore CS1591 - } - - /// - /// A refresh token request. + /// A personal access token. /// - /// The refresh token. - public record RefreshTokenRequest( - [Required] string RefreshToken); - + /// The token identifier. + /// The token secret. + /// The token description. + /// The date/time when the token expires. + /// The claims that will be part of the token. + public record PersonalAccessToken( + [property: JsonIgnore, ValidateNever] Guid Id, + [property: JsonIgnore] string Secret, + string Description, + DateTime Expires, + IDictionary Claims + ); + /// /// A revoke token request. /// - /// The refresh token. - public record RevokeTokenRequest( - [Required] string RefreshToken); - - /// - /// A token pair. - /// - /// The JWT token. - /// The refresh token. - public record TokenPair( - string AccessToken, - string RefreshToken); + /// The claims that will be part of the token. + /// The token description. + /// The date/time when the token expires. + public record CreateTokenRequest( + IDictionary Claims, + string Description, + DateTime Expires); /// /// Describes an OpenID connect provider. @@ -342,10 +288,10 @@ public record JobStatus( /// The user id. /// The user. /// A boolean which indicates if the user is an administrator. - /// A list of currently present refresh tokens. + /// A list of personal access tokens. public record MeResponse( string UserId, NexusUser User, bool IsAdmin, - IReadOnlyDictionary RefreshTokens); + IReadOnlyList PersonalAccessTokens); } diff --git a/src/Nexus/Core/NexusAuthExtensions.cs b/src/Nexus/Core/NexusAuthExtensions.cs index 98607afe..38608708 100644 --- a/src/Nexus/Core/NexusAuthExtensions.cs +++ b/src/Nexus/Core/NexusAuthExtensions.cs @@ -5,9 +5,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.DataProtection; using Microsoft.EntityFrameworkCore; -using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.IdentityModel.Protocols.OpenIdConnect; -using Microsoft.IdentityModel.Tokens; using Nexus.Core; using Nexus.Utilities; using System.Net; @@ -34,8 +32,6 @@ public static IServiceCollection AddNexusAuth( { /* https://stackoverflow.com/a/52493428/1636629 */ - JsonWebTokenHandler.DefaultInboundClaimTypeMap.Clear(); - services.AddDataProtection() .PersistKeysToFileSystem(new DirectoryInfo(Path.Combine(pathsOptions.Config, "data-protection-keys"))); @@ -56,21 +52,6 @@ public static IServiceCollection AddNexusAuth( context.Response.StatusCode = (int)HttpStatusCode.Forbidden; return Task.CompletedTask; }; - }) - - .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options => - { - options.TokenValidationParameters = new TokenValidationParameters() - { - NameClaimType = Claims.Name, - RoleClaimType = Claims.Role, - ClockSkew = TimeSpan.Zero, - ValidateAudience = false, - ValidateIssuer = false, - ValidateActor = false, - ValidateLifetime = true, - IssuerSigningKey = new SymmetricSecurityKey(Convert.FromBase64String(securityOptions.Base64JwtSigningKey)) - }; }); var providers = securityOptions.OidcProviders.Any() diff --git a/src/Nexus/Core/NexusOptions.cs b/src/Nexus/Core/NexusOptions.cs index 109d3f93..5b07b910 100644 --- a/src/Nexus/Core/NexusOptions.cs +++ b/src/Nexus/Core/NexusOptions.cs @@ -98,12 +98,8 @@ internal record OpenIdConnectProvider internal partial record SecurityOptions() : NexusOptionsBase { public const string Section = "Security"; - public const string DefaultSigningKey = "WOE6/wiy6E4UQJefC03ffOsBnilijFOjhFUw1eUtzhD/8/YNR7auSUeH+5VcGfXU4pki7ZLCulmvNq8c03S96g=="; - public string Base64JwtSigningKey { get; set; } = DefaultSigningKey; public TimeSpan CookieLifetime { get; set; } = TimeSpan.FromDays(30); - public TimeSpan AccessTokenLifetime { get; set; } = TimeSpan.FromHours(1); - public TimeSpan RefreshTokenLifetime { get; set; } = TimeSpan.MaxValue; public List OidcProviders { get; set; } = new(); } } \ No newline at end of file diff --git a/src/Nexus/Core/UserDbContext.cs b/src/Nexus/Core/UserDbContext.cs index c0108384..bfeb1cc7 100644 --- a/src/Nexus/Core/UserDbContext.cs +++ b/src/Nexus/Core/UserDbContext.cs @@ -12,14 +12,6 @@ public UserDbContext(DbContextOptions options) protected override void OnModelCreating(ModelBuilder modelBuilder) { - // this is required, otherwise when deleting claims or refresh tokens, they just get their OwnerId = null - // https://learn.microsoft.com/en-us/ef/core/modeling/relationships?tabs=fluent-api%2Cfluent-api-simple-key%2Csimple-key#required-and-optional-relationships - modelBuilder - .Entity() - .HasOne(token => token.Owner) - .WithMany(user => user.RefreshTokens) - .IsRequired(); - modelBuilder .Entity() .HasOne(claim => claim.Owner) @@ -29,8 +21,6 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) public DbSet Users { get; set; } = default!; - public DbSet RefreshTokens { get; set; } = default!; - public DbSet Claims { get; set; } = default!; } } diff --git a/src/Nexus/Nexus.csproj b/src/Nexus/Nexus.csproj index 8031e3a9..85669cad 100644 --- a/src/Nexus/Nexus.csproj +++ b/src/Nexus/Nexus.csproj @@ -35,14 +35,6 @@ - - - - diff --git a/src/Nexus/Program.cs b/src/Nexus/Program.cs index 88bde8b7..046cd085 100644 --- a/src/Nexus/Program.cs +++ b/src/Nexus/Program.cs @@ -43,13 +43,6 @@ Log.Logger = loggerConfiguration .CreateLogger(); -// checks -if (securityOptions.Base64JwtSigningKey == SecurityOptions.DefaultSigningKey) - Log.Logger.Warning("You are using the default key to sign JWT tokens. It is strongly advised to use a different key in production."); - -if (securityOptions.AccessTokenLifetime >= securityOptions.RefreshTokenLifetime) - Log.Logger.Warning("The refresh token life time should be greater than the access token lifetime."); - // run try { @@ -156,11 +149,11 @@ void AddServices( services.AddTransient(); services.AddScoped(); - services.AddScoped(); services.AddScoped(provider => provider.GetService()!.HttpContext!.User); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/src/Nexus/Services/DatabaseService.cs b/src/Nexus/Services/DatabaseService.cs index 4f60981e..380246ac 100644 --- a/src/Nexus/Services/DatabaseService.cs +++ b/src/Nexus/Services/DatabaseService.cs @@ -31,6 +31,12 @@ internal interface IDatabaseService 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 */ + IEnumerable EnumerateTokens(string userId); + bool TryReadToken(string userId, string tokenId, [NotNullWhen(true)] out string? token); + Stream WriteToken(string userId, string tokenId); + void DeleteToken(string userId, string tokenId); } internal class DatabaseService : IDatabaseService @@ -193,7 +199,6 @@ public void DeleteAttachment(string catalogId, string attachmentId) { var physicalId = catalogId.TrimStart('/').Replace("/", "_"); var attachmentFile = SafePathCombine(Path.Combine(_pathsOptions.Catalogs, physicalId), attachmentId); - _ = Path.GetDirectoryName(attachmentFile)!; File.Delete(attachmentFile); } @@ -329,6 +334,54 @@ private static async Task DeleteCacheEntryAsync(string cacheEntry, TimeSpan time throw new Exception($"Cannot delete cache entry {cacheEntry}."); } + /* /users */ + public IEnumerable EnumerateTokens(string userId) + { + var tokensFolder = Path.Combine(SafePathCombine(_pathsOptions.Users, userId), "tokens"); + + if (Directory.Exists(tokensFolder)) + return Directory + .EnumerateFiles(tokensFolder, "*", SearchOption.AllDirectories) + .Select(tokenFilePath => tokenFilePath[(tokensFolder.Length + 1)..]); + + else + return Enumerable.Empty(); + } + + public bool TryReadToken(string userId, string tokenId, [NotNullWhen(true)] out string? token) + { + var folderPath = Path.Combine(SafePathCombine(_pathsOptions.Users, userId), "tokens"); + var tokenFilePath = Path.Combine(folderPath, $"{tokenId}.json"); + + token = default; + + if (File.Exists(tokenFilePath)) + { + token = File.ReadAllText(tokenFilePath); + return true; + } + + return false; + } + + public Stream WriteToken(string userId, string tokenId) + { + var folderPath = Path.Combine(SafePathCombine(_pathsOptions.Users, userId), "tokens"); + var tokenFilePath = Path.Combine(folderPath, $"{tokenId}.json"); + + Directory.CreateDirectory(folderPath); + + return File.Open(tokenFilePath, FileMode.Create, FileAccess.Write); + } + + public void DeleteToken(string userId, string tokenId) + { + var folderPath = Path.Combine(SafePathCombine(_pathsOptions.Users, userId), "tokens"); + var tokenFilePath = Path.Combine(folderPath, $"{tokenId}.json"); + + File.Delete(tokenFilePath); + } + private static string SafePathCombine(string basePath, string relativePath) { var filePath = Path.GetFullPath(Path.Combine(basePath, relativePath)); diff --git a/src/Nexus/Services/DbService.cs b/src/Nexus/Services/DbService.cs index 6986bf7a..0131bb26 100644 --- a/src/Nexus/Services/DbService.cs +++ b/src/Nexus/Services/DbService.cs @@ -8,10 +8,8 @@ internal interface IDBService IQueryable GetUsers(); Task FindUserAsync(string userId); Task FindClaimAsync(Guid claimId); - Task FindRefreshTokenAsync(Guid tokenId, bool includeUserClaims); Task AddOrUpdateUserAsync(NexusUser user); Task AddOrUpdateClaimAsync(NexusClaim claim); - Task AddOrUpdateRefreshTokenAsync(RefreshToken token); Task DeleteUserAsync(string userId); Task SaveChangesAsync(); } @@ -28,15 +26,13 @@ public DbService( public IQueryable GetUsers() { - return _context.Users - .Include(user => user.RefreshTokens); + return _context.Users; } public Task FindUserAsync(string userId) { return _context.Users .Include(user => user.Claims) - .Include(user => user.RefreshTokens) .AsSingleQuery() .FirstOrDefaultAsync(user => user.Id == userId); @@ -64,27 +60,6 @@ public IQueryable GetUsers() return claim; } - public async Task FindRefreshTokenAsync(Guid tokenId, bool includeUserClaims) - { - RefreshToken? token; - - var query = _context.RefreshTokens - .Include(token => token.Owner); - - if (includeUserClaims) - token = await _context.RefreshTokens - .Include(token => token.Owner) - .ThenInclude(owner => owner.Claims) - .FirstOrDefaultAsync(token => token.Id == tokenId); - - else - token = await _context.RefreshTokens - .Include(token => token.Owner) - .FirstOrDefaultAsync(token => token.Id == tokenId); - - return token; - } - public async Task AddOrUpdateUserAsync(NexusUser user) { var reference = await _context.FindAsync(user.Id); @@ -111,19 +86,6 @@ public async Task AddOrUpdateClaimAsync(NexusClaim claim) await _context.SaveChangesAsync(); } - public async Task AddOrUpdateRefreshTokenAsync(RefreshToken token) - { - var reference = await _context.FindAsync(token.Id); - - if (reference is null) - _context.Add(token); - - else // https://stackoverflow.com/a/64094369 - _context.Entry(reference).CurrentValues.SetValues(token); - - await _context.SaveChangesAsync(); - } - public async Task DeleteUserAsync(string userId) { var user = await FindUserAsync(userId); diff --git a/src/Nexus/Services/NexusAuthenticationService.cs b/src/Nexus/Services/NexusAuthenticationService.cs deleted file mode 100644 index 8fdc2667..00000000 --- a/src/Nexus/Services/NexusAuthenticationService.cs +++ /dev/null @@ -1,190 +0,0 @@ -using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.Extensions.Options; -using Microsoft.IdentityModel.JsonWebTokens; -using Microsoft.IdentityModel.Tokens; -using Nexus.Core; -using System.Security.Claims; -using System.Security.Cryptography; -using static OpenIddict.Abstractions.OpenIddictConstants; - -namespace Nexus.Services -{ - // https://jasonwatmore.com/post/2021/06/15/net-5-api-jwt-authentication-with-refresh-tokens - // https://github.com/cornflourblue/dotnet-5-jwt-authentication-api - - internal interface INexusAuthenticationService - { - Task GenerateRefreshTokenAsync( - NexusUser user, - string description); - - Task RefreshTokenAsync( - RefreshToken token); - - Task RevokeTokenAsync( - RefreshToken token); - } - - internal class NexusAuthenticationService : INexusAuthenticationService - { - #region Fields - - private readonly IDBService _dbService; - private readonly SecurityOptions _securityOptions; - private readonly SigningCredentials _signingCredentials; - - #endregion - - #region Constructors - - public NexusAuthenticationService( - IDBService dbService, - IOptions securityOptions) - { - _dbService = dbService; - _securityOptions = securityOptions.Value; - - var key = Convert.FromBase64String(_securityOptions.Base64JwtSigningKey); - _signingCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature); - } - - #endregion - - #region Methods - - public async Task GenerateRefreshTokenAsync( - NexusUser user, string description) - { - // new refresh token - var newRefreshToken = GenerateRefreshToken( - description); - - user.RefreshTokens.Add(newRefreshToken); - - // add token - - /* 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.AddOrUpdateRefreshTokenAsync(newRefreshToken); - - return newRefreshToken.Token; - } - - public async Task RefreshTokenAsync(RefreshToken token) - { - var user = token.Owner; - - // new token pair - var newAccessToken = GenerateAccessToken( - user: user, - accessTokenLifeTime: _securityOptions.AccessTokenLifetime); - - var newRefreshToken = GenerateRefreshToken( - description: token.Description, - ancestor: token); - - // change token content - await _dbService.AddOrUpdateRefreshTokenAsync(newRefreshToken); - - return new TokenPair(newAccessToken, newRefreshToken.Token); - } - - public async Task RevokeTokenAsync(RefreshToken token) - { - // revoke token - token.Owner.RefreshTokens.Remove(token); - - // save changes - await _dbService.SaveChangesAsync(); - } - - #endregion - - #region Helper Methods - - private string GenerateAccessToken(NexusUser user, TimeSpan accessTokenLifeTime) - { - var mandatoryClaims = new[] - { - new Claim(Claims.Subject, user.Id), - new Claim(Claims.Name, user.Name) - }; - - var claims = user.Claims - .Select(entry => new Claim(entry.Type, entry.Value)); - - var claimsIdentity = new ClaimsIdentity( - mandatoryClaims.Concat(claims), - authenticationType: JwtBearerDefaults.AuthenticationScheme, - nameType: Claims.Name, - roleType: Claims.Role); - - // TODO: We will encounter the year 2038 problem if this is not solved (https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/92) - var utcNow = DateTime.UtcNow; - var year2038Limit = new DateTime(2038, 01, 19, 03, 14, 07, DateTimeKind.Utc); - - var combinedAccessTokenLifeTime = AddWillOverflow(utcNow.Ticks, accessTokenLifeTime.Ticks) - ? DateTime.MaxValue - : utcNow + accessTokenLifeTime; - - var limitedAccessTokenLifeTime = combinedAccessTokenLifeTime > year2038Limit - ? year2038Limit - : utcNow + accessTokenLifeTime; - - var tokenDescriptor = new SecurityTokenDescriptor - { - Subject = claimsIdentity, - NotBefore = DateTime.UtcNow, - Expires = limitedAccessTokenLifeTime, - SigningCredentials = _signingCredentials - }; - - var tokenHandler = new JsonWebTokenHandler(); - var token = tokenHandler.CreateToken(tokenDescriptor); - - return token; - } - - private RefreshToken GenerateRefreshToken(string description, RefreshToken? ancestor = default) - { - var expires = ancestor is null - ? _securityOptions.RefreshTokenLifetime == TimeSpan.MaxValue - ? DateTime.MaxValue - : DateTime.UtcNow.Add(_securityOptions.RefreshTokenLifetime) - : ancestor.Expires; - - var id = ancestor is null - ? Guid.NewGuid() - : ancestor.InternalRefreshToken.Id; - - var randomBytes = RandomNumberGenerator.GetBytes(64); - - var token = new InternalRefreshToken( - Version: 1, - Id: id, - Value: Convert.ToBase64String(randomBytes) - ); - - var serializedToken = InternalRefreshToken.Serialize(token); - - return new RefreshToken(id, serializedToken, expires, description); - } - - private static bool AddWillOverflow(long x, long y) - { - var willOverflow = false; - - if (x > 0 && y > 0 && y > (long.MaxValue - x)) - willOverflow = true; - - if (x < 0 && y < 0 && y < (long.MinValue - x)) - willOverflow = true; - - return willOverflow; - } - - #endregion - } -} diff --git a/src/Nexus/Services/TokenService.cs b/src/Nexus/Services/TokenService.cs new file mode 100644 index 00000000..7f360869 --- /dev/null +++ b/src/Nexus/Services/TokenService.cs @@ -0,0 +1,95 @@ +using System.Collections.Concurrent; +using System.Security.Cryptography; +using Nexus.Core; + +namespace Nexus.Services; + +internal interface ITokenService +{ + Task CreateAsync( + string userId, + string description, + DateTime expires, + IDictionary claims); + + Task DeleteAsync( + string userId, + Guid tokenId); + + Task DeleteAsync( + string tokenValue); + + Task> GetAllAsync( + string userId); +} + +internal class TokenService : ITokenService +{ + private static readonly RandomNumberGenerator _rng = RandomNumberGenerator.Create(); + + private readonly ConcurrentDictionary> _cache = new(); + + public Task CreateAsync( + string userId, + string description, + DateTime expires, + IDictionary claims) + { + var userMap = GetUserMap(userId); + var id = Guid.NewGuid(); + + Span secretBytes = stackalloc byte[64]; + _rng.GetBytes(secretBytes); + + var secret = Convert.ToBase64String(secretBytes); + + var token = new PersonalAccessToken( + id, + secret, + description, + expires, + claims + ); + + userMap.AddOrUpdate( + secret, + token, + (key, _) => token + ); + + var tokenValue = $"{userId}_{secret}"; + + return Task.FromResult(tokenValue); + } + + public Task DeleteAsync(string userId, Guid tokenId) + { + var userMap = GetUserMap(userId); + + + } + + public Task DeleteAsync(string tokenValue) + { + var userMap = GetUserMap(userId); + } + + public Task> GetAllAsync( + string userId) + { + var userMap = GetUserMap(userId); + + var result = userMap.ToDictionary( + entry => entry.Value.Id, + entry => entry.Value); + + return Task.FromResult((IDictionary)result); + } + + private ConcurrentDictionary GetUserMap(string userId) + { + return _cache.GetOrAdd( + userId, + key => new ConcurrentDictionary()); + } +} diff --git a/tests/Nexus.Tests/NexusAuthenticationServiceTests.cs b/tests/Nexus.Tests/NexusAuthenticationServiceTests.cs index a7138368..0e337a0b 100644 --- a/tests/Nexus.Tests/NexusAuthenticationServiceTests.cs +++ b/tests/Nexus.Tests/NexusAuthenticationServiceTests.cs @@ -9,7 +9,7 @@ namespace Services { - public class NexusAuthenticationServiceTests + public class NexusPersonalAccessTokenServiceTests { private static readonly IOptions _securityOptions = Options.Create(new SecurityOptions() { @@ -57,7 +57,7 @@ public async Task CanGenerateRefreshToken() var dbService = Mock.Of(); - var authService = new NexusAuthenticationService( + var authService = new NexusPersonalAccessTokenService( dbService, _securityOptions); _ = new JwtSecurityTokenHandler(); @@ -94,7 +94,7 @@ public async Task CanRefresh() var dbService = Mock.Of(); - var service = new NexusAuthenticationService( + var service = new NexusPersonalAccessTokenService( dbService, _securityOptions); @@ -129,7 +129,7 @@ public async Task CanRevoke() var dbService = Mock.Of(); - var service = new NexusAuthenticationService( + var service = new NexusPersonalAccessTokenService( dbService, _securityOptions); From 381eb2da36ced648095a24129b30671ee9c2d9dd Mon Sep 17 00:00:00 2001 From: Apollo3zehn Date: Thu, 7 Mar 2024 19:05:00 +0100 Subject: [PATCH 03/19] Nearly done --- src/Nexus/API/JobsController.cs | 2 +- src/Nexus/API/UsersController.cs | 71 ++++----- src/Nexus/Core/CatalogContainer.cs | 2 +- src/Nexus/Core/Models_NonPublic.cs | 3 +- src/Nexus/Core/Models_Public.cs | 23 +-- src/Nexus/Services/AppStateManager.cs | 2 +- src/Nexus/Services/DatabaseService.cs | 47 +++--- src/Nexus/Services/TokenService.cs | 138 ++++++++++++----- src/Nexus/Utilities/JsonSerializerHelper.cs | 9 +- .../NexusAuthenticationServiceTests.cs | 143 ------------------ tests/Nexus.Tests/Other/UtilitiesTests.cs | 2 +- .../Services/CatalogManagerTests.cs | 4 +- .../Nexus.Tests/Services/TokenServiceTests.cs | 78 ++++++++++ 13 files changed, 258 insertions(+), 266 deletions(-) delete mode 100644 tests/Nexus.Tests/NexusAuthenticationServiceTests.cs create mode 100644 tests/Nexus.Tests/Services/TokenServiceTests.cs diff --git a/src/Nexus/API/JobsController.cs b/src/Nexus/API/JobsController.cs index b2f54ec2..f7648f4f 100644 --- a/src/Nexus/API/JobsController.cs +++ b/src/Nexus/API/JobsController.cs @@ -167,7 +167,7 @@ public async Task> ExportAsync( ExportParameters parameters, CancellationToken cancellationToken) { - _diagnosticContext.Set("Body", JsonSerializerHelper.SerializeIntended(parameters)); + _diagnosticContext.Set("Body", JsonSerializerHelper.SerializeIndented(parameters)); parameters = parameters with { diff --git a/src/Nexus/API/UsersController.cs b/src/Nexus/API/UsersController.cs index 0e593b30..0949c831 100644 --- a/src/Nexus/API/UsersController.cs +++ b/src/Nexus/API/UsersController.cs @@ -134,17 +134,7 @@ public async Task SignOutAsync( public async Task DeleteTokenByValueAsync( [BindRequired] string value) { - // get token - // var internalRefreshToken = InternalRefreshToken.Deserialize(); - // var token = await _dbService.FindRefreshTokenAsync(internalRefreshToken.Id, includeUserClaims: false); - - // if (token is null) - // return NotFound("Token not found."); - - // // revoke token - // await _authService - // .RevokeTokenAsync(token); - + await _tokenService.DeleteAsync(value); return Ok(); } @@ -194,10 +184,10 @@ public async Task> CreateTokenAsync( if (user is null) return NotFound($"Could not find user {userId}."); - // var refreshToken = await _authService.GenerateRefreshTokenAsync(user, description); + await _tokenService + .CreateAsync(actualUserId, request.Description, request.Expires, request.Claims); - // return Ok(refreshToken); - throw new Exception(); + return Ok(); } else @@ -206,6 +196,21 @@ public async Task> CreateTokenAsync( } } + /// + /// 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); + + return Ok(); + } + /// /// Accepts the license of the specified catalog. /// @@ -244,28 +249,6 @@ public async Task AcceptLicenseAsync( return Redirect(redirectUrl); } - /// - /// Deletes a personal access token. - /// - /// The identifier of the personal access token. - [HttpDelete("tokens/{tokenId}")] - public async Task DeleteTokenAsync( - Guid tokenId) - { - // // TODO: Is this thread safe? Maybe yes, because of scoped EF context. - - // var token = await _dbService.FindRefreshTokenAsync(tokenId, includeUserClaims: true); - - // if (token is null) - // return NotFound($"Could not find refresh token {tokenId}."); - - // token.Owner.RefreshTokens.Remove(token); - - // await _dbService.SaveChangesAsync(); - - return Ok(); - } - #endregion #region Privileged @@ -389,7 +372,7 @@ public async Task DeleteClaimAsync( /// The identifier of the user. [Authorize(Policy = NexusPolicies.RequireAdmin)] [HttpGet("{userId}/tokens")] - public async Task>> GetPersonalAccessTokensAsync( + public async Task>> GetTokensAsync( string userId) { var user = await _dbService.FindUserAsync(userId); @@ -397,8 +380,16 @@ public async Task>> GetPersonalA if (user is null) return NotFound($"Could not find user {userId}."); - // return Ok(user.RefreshTokens.ToDictionary(token => token.Id, token => token)); - throw new Exception(); + var tokenMap = await _tokenService.GetAllAsync(userId); + + var translatedTokenMap = tokenMap + .ToDictionary(entry => entry.Key, entry => new PersonalAccessToken( + entry.Value.Description, + entry.Value.Expires, + entry.Value.Claims + )); + + return Ok(translatedTokenMap); } private bool TryAuthenticate( @@ -413,7 +404,7 @@ private bool TryAuthenticate( response = null; else - response = StatusCode(StatusCodes.Status403Forbidden, $"The current user is not permitted to get source registrations of user {requestedId}."); + response = StatusCode(StatusCodes.Status403Forbidden, $"The current user is not permitted to perform the operation for user {requestedId}."); userId = requestedId is null ? currentId diff --git a/src/Nexus/Core/CatalogContainer.cs b/src/Nexus/Core/CatalogContainer.cs index 75be52a8..fe1c606d 100644 --- a/src/Nexus/Core/CatalogContainer.cs +++ b/src/Nexus/Core/CatalogContainer.cs @@ -120,7 +120,7 @@ public async Task UpdateMetadataAsync(CatalogMetadata metadata) { // persist using var stream = _databaseService.WriteCatalogMetadata(Id); - await JsonSerializerHelper.SerializeIntendedAsync(stream, metadata); + await JsonSerializerHelper.SerializeIndentedAsync(stream, metadata); // assign Metadata = metadata; diff --git a/src/Nexus/Core/Models_NonPublic.cs b/src/Nexus/Core/Models_NonPublic.cs index b46fab19..dff2c501 100644 --- a/src/Nexus/Core/Models_NonPublic.cs +++ b/src/Nexus/Core/Models_NonPublic.cs @@ -7,10 +7,9 @@ namespace Nexus.Core { internal record InternalPersonalAccessToken ( Guid Id, - byte[] Secret, string Description, DateTime Expires, - IDictionary Claims); + IReadOnlyList Claims); internal record struct Interval( DateTime Begin, diff --git a/src/Nexus/Core/Models_Public.cs b/src/Nexus/Core/Models_Public.cs index 4435b860..8a14a767 100644 --- a/src/Nexus/Core/Models_Public.cs +++ b/src/Nexus/Core/Models_Public.cs @@ -85,29 +85,34 @@ public NexusClaim(Guid id, string type, string value) /// /// A personal access token. /// - /// The token identifier. - /// The token secret. /// The token description. /// The date/time when the token expires. /// The claims that will be part of the token. public record PersonalAccessToken( - [property: JsonIgnore, ValidateNever] Guid Id, - [property: JsonIgnore] string Secret, string Description, DateTime Expires, - IDictionary Claims + IReadOnlyList Claims ); - + /// /// A revoke token request. /// - /// The claims that will be part of the token. /// The token description. /// The date/time when the token expires. + /// The claims that will be part of the token. public record CreateTokenRequest( - IDictionary Claims, string Description, - DateTime Expires); + 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. diff --git a/src/Nexus/Services/AppStateManager.cs b/src/Nexus/Services/AppStateManager.cs index a1d257ea..0ffae093 100644 --- a/src/Nexus/Services/AppStateManager.cs +++ b/src/Nexus/Services/AppStateManager.cs @@ -306,7 +306,7 @@ private void LoadDataWriters() private async Task SaveProjectAsync(NexusProject project) { using var stream = _databaseService.WriteProject(); - await JsonSerializerHelper.SerializeIntendedAsync(stream, project); + await JsonSerializerHelper.SerializeIndentedAsync(stream, project); } #endregion diff --git a/src/Nexus/Services/DatabaseService.cs b/src/Nexus/Services/DatabaseService.cs index 380246ac..2641f4ee 100644 --- a/src/Nexus/Services/DatabaseService.cs +++ b/src/Nexus/Services/DatabaseService.cs @@ -33,10 +33,15 @@ internal interface IDatabaseService Task ClearCacheEntriesAsync(string catalogId, DateOnly day, TimeSpan timeout, Predicate predicate); /* /users */ - IEnumerable EnumerateTokens(string userId); - bool TryReadToken(string userId, string tokenId, [NotNullWhen(true)] out string? token); - Stream WriteToken(string userId, string tokenId); - void DeleteToken(string userId, string tokenId); + bool TryReadTokenMap( + string userId, + [NotNullWhen(true)] out string? tokenMap); + + Stream WriteTokenMap( + string userId); + + void DeleteTokenMap( + string userId); } internal class DatabaseService : IDatabaseService @@ -335,53 +340,45 @@ private static async Task DeleteCacheEntryAsync(string cacheEntry, TimeSpan time } /* /users */ - public IEnumerable EnumerateTokens(string userId) - { - var tokensFolder = Path.Combine(SafePathCombine(_pathsOptions.Users, userId), "tokens"); - - if (Directory.Exists(tokensFolder)) - return Directory - .EnumerateFiles(tokensFolder, "*", SearchOption.AllDirectories) - .Select(tokenFilePath => tokenFilePath[(tokensFolder.Length + 1)..]); - - else - return Enumerable.Empty(); - } - - public bool TryReadToken(string userId, string tokenId, [NotNullWhen(true)] out string? token) + public bool TryReadTokenMap( + string userId, + [NotNullWhen(true)] out string? tokenMap) { var folderPath = Path.Combine(SafePathCombine(_pathsOptions.Users, userId), "tokens"); - var tokenFilePath = Path.Combine(folderPath, $"{tokenId}.json"); + var tokenFilePath = Path.Combine(folderPath, "tokens.json"); - token = default; + tokenMap = default; if (File.Exists(tokenFilePath)) { - token = File.ReadAllText(tokenFilePath); + tokenMap = File.ReadAllText(tokenFilePath); return true; } return false; } - public Stream WriteToken(string userId, string tokenId) + public Stream WriteTokenMap( + string userId) { var folderPath = Path.Combine(SafePathCombine(_pathsOptions.Users, userId), "tokens"); - var tokenFilePath = Path.Combine(folderPath, $"{tokenId}.json"); + var tokenFilePath = Path.Combine(folderPath, "tokens.json"); Directory.CreateDirectory(folderPath); return File.Open(tokenFilePath, FileMode.Create, FileAccess.Write); } - public void DeleteToken(string userId, string tokenId) + public void DeleteTokenMap( + string userId) { var folderPath = Path.Combine(SafePathCombine(_pathsOptions.Users, userId), "tokens"); - var tokenFilePath = Path.Combine(folderPath, $"{tokenId}.json"); + var tokenFilePath = Path.Combine(folderPath, "tokens.json"); File.Delete(tokenFilePath); } + // private static string SafePathCombine(string basePath, string relativePath) { var filePath = Path.GetFullPath(Path.Combine(basePath, relativePath)); diff --git a/src/Nexus/Services/TokenService.cs b/src/Nexus/Services/TokenService.cs index 7f360869..f9208da1 100644 --- a/src/Nexus/Services/TokenService.cs +++ b/src/Nexus/Services/TokenService.cs @@ -1,6 +1,8 @@ using System.Collections.Concurrent; using System.Security.Cryptography; +using System.Text.Json; using Nexus.Core; +using Nexus.Utilities; namespace Nexus.Services; @@ -10,7 +12,7 @@ Task CreateAsync( string userId, string description, DateTime expires, - IDictionary claims); + IReadOnlyList claims); Task DeleteAsync( string userId, @@ -19,7 +21,7 @@ Task DeleteAsync( Task DeleteAsync( string tokenValue); - Task> GetAllAsync( + Task> GetAllAsync( string userId); } @@ -27,69 +29,127 @@ internal class TokenService : ITokenService { private static readonly RandomNumberGenerator _rng = RandomNumberGenerator.Create(); - private readonly ConcurrentDictionary> _cache = new(); + private readonly SemaphoreSlim _semaphoreSlim = new SemaphoreSlim(1, 1); + + private readonly ConcurrentDictionary> _cache = new(); + + private readonly IDatabaseService _databaseService; + + public TokenService(IDatabaseService databaseService) + { + _databaseService = databaseService; + } public Task CreateAsync( string userId, string description, DateTime expires, - IDictionary claims) + IReadOnlyList claims) { - var userMap = GetUserMap(userId); - var id = Guid.NewGuid(); + return UpdateTokenMapAsync(userId, tokenMap => + { + var id = Guid.NewGuid(); - Span secretBytes = stackalloc byte[64]; - _rng.GetBytes(secretBytes); + Span secretBytes = stackalloc byte[64]; + _rng.GetBytes(secretBytes); - var secret = Convert.ToBase64String(secretBytes); + var secret = Convert.ToBase64String(secretBytes); - var token = new PersonalAccessToken( - id, - secret, - description, - expires, - claims - ); + var token = new InternalPersonalAccessToken( + id, + description, + expires, + claims + ); - userMap.AddOrUpdate( - secret, - token, - (key, _) => token - ); + tokenMap.AddOrUpdate( + secret, + token, + (key, _) => token + ); - var tokenValue = $"{userId}_{secret}"; + var tokenValue = $"{secret}_{userId}"; - return Task.FromResult(tokenValue); + return tokenValue; + }, saveChanges: true); } public Task DeleteAsync(string userId, Guid tokenId) { - var userMap = GetUserMap(userId); - - + return UpdateTokenMapAsync(userId, tokenMap => + { + var tokenEntry = tokenMap + .FirstOrDefault(entry => entry.Value.Id == tokenId); + + tokenMap.TryRemove(tokenEntry.Key, out _); + return default; + }, saveChanges: true); } public Task DeleteAsync(string tokenValue) { - var userMap = GetUserMap(userId); + var splittedTokenValue = tokenValue.Split('_', count: 1); + var userId = splittedTokenValue[0]; + var secret = splittedTokenValue[1]; + + return UpdateTokenMapAsync(userId, tokenMap => + { + tokenMap.TryRemove(secret, out _); + return default; + }, saveChanges: true); } - public Task> GetAllAsync( + public Task> GetAllAsync( string userId) { - var userMap = GetUserMap(userId); - - var result = userMap.ToDictionary( - entry => entry.Value.Id, - entry => entry.Value); - - return Task.FromResult((IDictionary)result); + return UpdateTokenMapAsync(userId, tokenMap => + { + var result = tokenMap.ToDictionary( + entry => entry.Value.Id, + entry => entry.Value); + + return (IDictionary)result; + }, saveChanges: false); } - private ConcurrentDictionary GetUserMap(string userId) + private async Task UpdateTokenMapAsync( + string userId, + Func, T> func, + bool saveChanges) { - return _cache.GetOrAdd( - userId, - key => new ConcurrentDictionary()); + await _semaphoreSlim.WaitAsync().ConfigureAwait(false); + + try + { + var tokenMap = _cache.GetOrAdd( + userId, + key => + { + if (_databaseService.TryReadTokenMap(userId, out var jsonString)) + { + return JsonSerializer.Deserialize>(jsonString) + ?? throw new Exception("tokenMap is null"); + } + + else + { + return new ConcurrentDictionary(); + } + }); + + var result = func(tokenMap); + + if (saveChanges) + { + using var stream = _databaseService.WriteTokenMap(userId); + JsonSerializerHelper.SerializeIndented(stream, tokenMap); + } + + return result; + } + finally + { + _semaphoreSlim.Release(); + } } } diff --git a/src/Nexus/Utilities/JsonSerializerHelper.cs b/src/Nexus/Utilities/JsonSerializerHelper.cs index 1b3e5342..e0761c1e 100644 --- a/src/Nexus/Utilities/JsonSerializerHelper.cs +++ b/src/Nexus/Utilities/JsonSerializerHelper.cs @@ -13,12 +13,17 @@ internal static class JsonSerializerHelper Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }; - public static string SerializeIntended(T value) + public static string SerializeIndented(T value) { return JsonSerializer.Serialize(value, _options); } - public static Task SerializeIntendedAsync(Stream utf8Json, T value) + 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); } diff --git a/tests/Nexus.Tests/NexusAuthenticationServiceTests.cs b/tests/Nexus.Tests/NexusAuthenticationServiceTests.cs deleted file mode 100644 index 0e337a0b..00000000 --- a/tests/Nexus.Tests/NexusAuthenticationServiceTests.cs +++ /dev/null @@ -1,143 +0,0 @@ -using Microsoft.Extensions.Options; -using Microsoft.IdentityModel.Tokens; -using Moq; -using Nexus.Core; -using Nexus.Services; -using System.IdentityModel.Tokens.Jwt; -using Xunit; -using static OpenIddict.Abstractions.OpenIddictConstants; - -namespace Services -{ - public class NexusPersonalAccessTokenServiceTests - { - private static readonly IOptions _securityOptions = Options.Create(new SecurityOptions() - { - AccessTokenLifetime = TimeSpan.FromHours(1), - RefreshTokenLifetime = TimeSpan.FromHours(1) - }); - - private static readonly TokenValidationParameters _validationParameters = new() - { - NameClaimType = Claims.Name, - LifetimeValidator = (before, expires, token, parameters) => expires > DateTime.UtcNow, - ValidateAudience = false, - ValidateIssuer = false, - ValidateActor = false, - ValidateLifetime = true, - IssuerSigningKey = new SymmetricSecurityKey(Convert.FromBase64String(SecurityOptions.DefaultSigningKey)) - }; - - private static NexusUser CreateUser(string name, params RefreshToken[] refreshTokens) - { - JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); - - var user = new NexusUser( - id: Guid.NewGuid().ToString(), - name: name - ) - { - RefreshTokens = refreshTokens.ToList() - }; - - foreach (var token in refreshTokens) - { - token.Owner = user; - } - - return user; - } - - [Fact] - public async Task CanGenerateRefreshToken() - { - // Arrange - var expectedName = "foo"; - var user = CreateUser(expectedName); - - var dbService = Mock.Of(); - - var authService = new NexusPersonalAccessTokenService( - dbService, - _securityOptions); - _ = new JwtSecurityTokenHandler(); - - // Act - var refreshToken = await authService.GenerateRefreshTokenAsync(user, string.Empty); - - // Assert - Assert.Single(user.RefreshTokens); - Assert.Equal(refreshToken, user.RefreshTokens.First().Token); - - Assert.Equal( - user.RefreshTokens.First().Expires, - DateTime.UtcNow.Add(_securityOptions.Value.RefreshTokenLifetime), - TimeSpan.FromMinutes(1)); - } - - [Fact] - public async Task CanRefresh() - { - // Arrange - var expectedName = "foo"; - - var internalRefreshToken = new InternalRefreshToken( - Version: 1, - Id: Guid.NewGuid(), - Value: string.Empty - ); - - var serializedToken = InternalRefreshToken.Serialize(internalRefreshToken); - var storedRefreshToken = new RefreshToken(Guid.NewGuid(), serializedToken, DateTime.UtcNow.AddDays(1), string.Empty); - var user = CreateUser(expectedName, storedRefreshToken); - storedRefreshToken.Owner = user; - - var dbService = Mock.Of(); - - var service = new NexusPersonalAccessTokenService( - dbService, - _securityOptions); - - var tokenHandler = new JwtSecurityTokenHandler(); - - // Act - var tokenPair = await service.RefreshTokenAsync(storedRefreshToken); - - // Assert - Assert.Single(user.RefreshTokens); - - var principal = tokenHandler.ValidateToken(tokenPair.AccessToken, _validationParameters, out var _); - var actualName = principal.Identity!.Name; - - Assert.Equal(expectedName, actualName); - } - - [Fact] - public async Task CanRevoke() - { - // Arrange - var internalRefreshToken = new InternalRefreshToken( - Version: 1, - Id: Guid.NewGuid(), - Value: string.Empty - ); - - var serializedToken = InternalRefreshToken.Serialize(internalRefreshToken); - var storedRefreshToken = new RefreshToken(Guid.NewGuid(), serializedToken, DateTime.UtcNow.AddDays(1), string.Empty); - var user = CreateUser("foo", storedRefreshToken); - storedRefreshToken.Owner = user; - - var dbService = Mock.Of(); - - var service = new NexusPersonalAccessTokenService( - dbService, - _securityOptions); - - // Act - await service.RevokeTokenAsync(storedRefreshToken); - - // Assert - Assert.Empty(user.RefreshTokens); - } - } -} \ No newline at end of file diff --git a/tests/Nexus.Tests/Other/UtilitiesTests.cs b/tests/Nexus.Tests/Other/UtilitiesTests.cs index a520aa7b..b8582ede 100644 --- a/tests/Nexus.Tests/Other/UtilitiesTests.cs +++ b/tests/Nexus.Tests/Other/UtilitiesTests.cs @@ -213,7 +213,7 @@ public void CanSerializeAndDeserializeTimeSpan() var expected = new MyType(A: 1, B: "Two", C: TimeSpan.FromSeconds(1)); // Act - var jsonString = JsonSerializerHelper.SerializeIntended(expected); + var jsonString = JsonSerializerHelper.SerializeIndented(expected); var actual = JsonSerializer.Deserialize(jsonString); // Assert diff --git a/tests/Nexus.Tests/Services/CatalogManagerTests.cs b/tests/Nexus.Tests/Services/CatalogManagerTests.cs index 51f07b7e..e857200e 100644 --- a/tests/Nexus.Tests/Services/CatalogManagerTests.cs +++ b/tests/Nexus.Tests/Services/CatalogManagerTests.cs @@ -275,8 +275,8 @@ public async Task CanLoadLazyCatalogInfos() var lazyCatalogInfo = await catalogContainer.GetLazyCatalogInfoAsync(CancellationToken.None); // Assert - var actualJsonString = JsonSerializerHelper.SerializeIntended(lazyCatalogInfo.Catalog); - var expectedJsonString = JsonSerializerHelper.SerializeIntended(expectedCatalog); + var actualJsonString = JsonSerializerHelper.SerializeIndented(lazyCatalogInfo.Catalog); + var expectedJsonString = JsonSerializerHelper.SerializeIndented(expectedCatalog); Assert.Equal(actualJsonString, expectedJsonString); Assert.Equal(new DateTime(2020, 01, 01), lazyCatalogInfo.Begin); diff --git a/tests/Nexus.Tests/Services/TokenServiceTests.cs b/tests/Nexus.Tests/Services/TokenServiceTests.cs new file mode 100644 index 00000000..e2603134 --- /dev/null +++ b/tests/Nexus.Tests/Services/TokenServiceTests.cs @@ -0,0 +1,78 @@ +using System.Text.Json; +using Moq; +using Nexus.Core; +using Nexus.Services; +using Xunit; + +namespace Services; + +public class TokenServiceTests +{ + delegate bool GobbleReturns(string userId, out string tokenMap); + + [Fact] + public async Task CanCreateToken() + { + // Arrange + var databaseService = Mock.Of(); + + Mock.Get(databaseService) + .Setup(databaseService => databaseService.TryReadTokenMap(It.IsAny(), out It.Ref.IsAny)) + .Returns(new GobbleReturns((string userId, out string tokenMap) => + { + tokenMap = JsonSerializer.Serialize(new Dictionary()); + return true; + })); + + var filePath = Path.GetTempFileName(); + + Mock.Get(databaseService) + .Setup(databaseService => databaseService.WriteTokenMap(It.IsAny())) + .Returns(File.OpenWrite(filePath)); + + var tokenService = new TokenService(databaseService); + + // Act + var description = "The description."; + var expires = new DateTime(2020, 01, 01); + var claim1Type = "claim1"; + var claim1Value = "value1"; + var claim2Type = "claim2"; + var claim2Value = "value2"; + + await tokenService.CreateAsync( + userId: "starlord", + description, + expires, + new List + { + new TokenClaim(claim1Type, claim1Value), + new TokenClaim(claim2Type, claim2Value), + } + ); + + // Assert + var jsonString = File.ReadAllText(filePath); + var actualTokenMap = JsonSerializer.Deserialize>(jsonString)!; + + Assert.Collection( + actualTokenMap, + entry1 => + { + Assert.Equal(description, entry1.Value.Description); + Assert.Equal(expires, entry1.Value.Expires); + + Assert.Collection(entry1.Value.Claims, + entry1_1 => + { + Assert.Equal(claim1Type, entry1_1.Type); + Assert.Equal(claim1Value, entry1_1.Value); + }, + entry1_2 => + { + Assert.Equal(claim2Type, entry1_2.Type); + Assert.Equal(claim2Value, entry1_2.Value); + }); + }); + } +} \ No newline at end of file From 6a9666b684ccf4cc6b1936d0a3d28499ab0fdb59 Mon Sep 17 00:00:00 2001 From: Apollo3zehn Date: Thu, 7 Mar 2024 19:05:00 +0100 Subject: [PATCH 04/19] Add tests --- src/Nexus/Services/DatabaseService.cs | 12 -- src/Nexus/Services/TokenService.cs | 6 +- .../Nexus.Tests/Services/TokenServiceTests.cs | 152 +++++++++++++++--- 3 files changed, 137 insertions(+), 33 deletions(-) diff --git a/src/Nexus/Services/DatabaseService.cs b/src/Nexus/Services/DatabaseService.cs index 2641f4ee..3d843179 100644 --- a/src/Nexus/Services/DatabaseService.cs +++ b/src/Nexus/Services/DatabaseService.cs @@ -39,9 +39,6 @@ bool TryReadTokenMap( Stream WriteTokenMap( string userId); - - void DeleteTokenMap( - string userId); } internal class DatabaseService : IDatabaseService @@ -369,15 +366,6 @@ public Stream WriteTokenMap( return File.Open(tokenFilePath, FileMode.Create, FileAccess.Write); } - public void DeleteTokenMap( - string userId) - { - var folderPath = Path.Combine(SafePathCombine(_pathsOptions.Users, userId), "tokens"); - var tokenFilePath = Path.Combine(folderPath, "tokens.json"); - - File.Delete(tokenFilePath); - } - // private static string SafePathCombine(string basePath, string relativePath) { diff --git a/src/Nexus/Services/TokenService.cs b/src/Nexus/Services/TokenService.cs index f9208da1..c422993e 100644 --- a/src/Nexus/Services/TokenService.cs +++ b/src/Nexus/Services/TokenService.cs @@ -88,9 +88,9 @@ public Task DeleteAsync(string userId, Guid tokenId) public Task DeleteAsync(string tokenValue) { - var splittedTokenValue = tokenValue.Split('_', count: 1); - var userId = splittedTokenValue[0]; - var secret = splittedTokenValue[1]; + var splittedTokenValue = tokenValue.Split('_', count: 2); + var secret = splittedTokenValue[0]; + var userId = splittedTokenValue[1]; return UpdateTokenMapAsync(userId, tokenMap => { diff --git a/tests/Nexus.Tests/Services/TokenServiceTests.cs b/tests/Nexus.Tests/Services/TokenServiceTests.cs index e2603134..378caff7 100644 --- a/tests/Nexus.Tests/Services/TokenServiceTests.cs +++ b/tests/Nexus.Tests/Services/TokenServiceTests.cs @@ -2,6 +2,7 @@ using Moq; using Nexus.Core; using Nexus.Services; +using Nexus.Utilities; using Xunit; namespace Services; @@ -14,25 +15,8 @@ public class TokenServiceTests public async Task CanCreateToken() { // Arrange - var databaseService = Mock.Of(); - - Mock.Get(databaseService) - .Setup(databaseService => databaseService.TryReadTokenMap(It.IsAny(), out It.Ref.IsAny)) - .Returns(new GobbleReturns((string userId, out string tokenMap) => - { - tokenMap = JsonSerializer.Serialize(new Dictionary()); - return true; - })); - var filePath = Path.GetTempFileName(); - - Mock.Get(databaseService) - .Setup(databaseService => databaseService.WriteTokenMap(It.IsAny())) - .Returns(File.OpenWrite(filePath)); - - var tokenService = new TokenService(databaseService); - - // Act + var tokenService = GetTokenService(filePath, new()); var description = "The description."; var expires = new DateTime(2020, 01, 01); var claim1Type = "claim1"; @@ -40,6 +24,7 @@ public async Task CanCreateToken() var claim2Type = "claim2"; var claim2Value = "value2"; + // Act await tokenService.CreateAsync( userId: "starlord", description, @@ -75,4 +60,135 @@ await tokenService.CreateAsync( }); }); } + + [Fact] + public async Task CanDeleteTokenByValue() + { + // Arrange + var id1 = Guid.NewGuid(); + var id2 = Guid.NewGuid(); + + var tokenMap = new Dictionary() + { + ["abc"] = new InternalPersonalAccessToken( + id1, + Description: string.Empty, + Expires: default, + Claims: new List() + ), + ["def"] = new InternalPersonalAccessToken( + id2, + Description: string.Empty, + Expires: default, + Claims: new List() + ) + }; + + var filePath = Path.GetTempFileName(); + var tokenService = GetTokenService(filePath, tokenMap); + + // Act + await tokenService.DeleteAsync("abc_userid"); + + // Assert + tokenMap.Remove("abc"); + var expected = JsonSerializerHelper.SerializeIndented(tokenMap); + var actual = File.ReadAllText(filePath); + + Assert.Equal(expected, actual); + } + + [Fact] + public async Task CanDeleteTokenById() + { + // Arrange + var id1 = Guid.NewGuid(); + var id2 = Guid.NewGuid(); + + var tokenMap = new Dictionary() + { + ["abc"] = new InternalPersonalAccessToken( + id1, + Description: string.Empty, + Expires: default, + Claims: new List() + ), + ["def"] = new InternalPersonalAccessToken( + id2, + Description: string.Empty, + Expires: default, + Claims: new List() + ) + }; + + var filePath = Path.GetTempFileName(); + var tokenService = GetTokenService(filePath, tokenMap); + + // Act + await tokenService.DeleteAsync(string.Empty, id1); + + // Assert + tokenMap.Remove("abc"); + var expected = JsonSerializerHelper.SerializeIndented(tokenMap); + var actual = File.ReadAllText(filePath); + + Assert.Equal(expected, actual); + } + + [Fact] + public async Task CanGetAllTokens() + { + // Arrange + var id1 = Guid.NewGuid(); + var id2 = Guid.NewGuid(); + + var expectedTokenMap = new Dictionary() + { + ["abc"] = new InternalPersonalAccessToken( + id1, + Description: string.Empty, + Expires: default, + Claims: new List() + ), + ["def"] = new InternalPersonalAccessToken( + id2, + Description: string.Empty, + Expires: default, + Claims: new List() + ) + }; + + var filePath = Path.GetTempFileName(); + var tokenService = GetTokenService(filePath, expectedTokenMap); + + // Act + var actualTokenMap = await tokenService.GetAllAsync(string.Empty); + + // Assert + var expected = JsonSerializerHelper.SerializeIndented(expectedTokenMap.ToDictionary(entry => entry.Value.Id, entry => entry.Value)); + var actual = JsonSerializerHelper.SerializeIndented(actualTokenMap); + + Assert.Equal(expected, actual); + } + + private ITokenService GetTokenService(string filePath, Dictionary tokenMap) + { + var databaseService = Mock.Of(); + + Mock.Get(databaseService) + .Setup(databaseService => databaseService.TryReadTokenMap(It.IsAny(), out It.Ref.IsAny)) + .Returns(new GobbleReturns((string userId, out string tokenMapString) => + { + tokenMapString = JsonSerializer.Serialize(tokenMap); + return true; + })); + + Mock.Get(databaseService) + .Setup(databaseService => databaseService.WriteTokenMap(It.IsAny())) + .Returns(File.OpenWrite(filePath)); + + var tokenService = new TokenService(databaseService); + + return tokenService; + } } \ No newline at end of file From e66b3572953fa8c4ebe2cf8587af4622c17a91eb Mon Sep 17 00:00:00 2001 From: Apollo3zehn Date: Thu, 7 Mar 2024 19:05:00 +0100 Subject: [PATCH 05/19] Some fixes --- src/Nexus/API/UsersController.cs | 17 +++++++++++++---- src/Nexus/Core/Models_Public.cs | 2 +- src/Nexus/Services/TokenService.cs | 16 ++++++---------- tests/Nexus.Tests/Services/TokenServiceTests.cs | 2 +- 4 files changed, 21 insertions(+), 16 deletions(-) diff --git a/src/Nexus/API/UsersController.cs b/src/Nexus/API/UsersController.cs index 0949c831..3ce15b8b 100644 --- a/src/Nexus/API/UsersController.cs +++ b/src/Nexus/API/UsersController.cs @@ -158,11 +158,20 @@ public async Task> GetMeAsync() 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, - default!); // user.RefreshTokens.ToDictionary(entry => entry.Id, entry => entry)); + translatedTokenMap); } /// @@ -372,7 +381,7 @@ public async Task DeleteClaimAsync( /// The identifier of the user. [Authorize(Policy = NexusPolicies.RequireAdmin)] [HttpGet("{userId}/tokens")] - public async Task>> GetTokensAsync( + public async Task>> GetTokensAsync( string userId) { var user = await _dbService.FindUserAsync(userId); @@ -383,13 +392,13 @@ public async Task>> GetTokensAsy var tokenMap = await _tokenService.GetAllAsync(userId); var translatedTokenMap = tokenMap - .ToDictionary(entry => entry.Key, entry => new PersonalAccessToken( + .ToDictionary(entry => entry.Value.Id, entry => new PersonalAccessToken( entry.Value.Description, entry.Value.Expires, entry.Value.Claims )); - return Ok(translatedTokenMap); + return translatedTokenMap; } private bool TryAuthenticate( diff --git a/src/Nexus/Core/Models_Public.cs b/src/Nexus/Core/Models_Public.cs index 8a14a767..17d0deda 100644 --- a/src/Nexus/Core/Models_Public.cs +++ b/src/Nexus/Core/Models_Public.cs @@ -298,5 +298,5 @@ public record MeResponse( string UserId, NexusUser User, bool IsAdmin, - IReadOnlyList PersonalAccessTokens); + IReadOnlyDictionary PersonalAccessTokens); } diff --git a/src/Nexus/Services/TokenService.cs b/src/Nexus/Services/TokenService.cs index c422993e..f4c491c6 100644 --- a/src/Nexus/Services/TokenService.cs +++ b/src/Nexus/Services/TokenService.cs @@ -21,7 +21,7 @@ Task DeleteAsync( Task DeleteAsync( string tokenValue); - Task> GetAllAsync( + Task> GetAllAsync( string userId); } @@ -99,17 +99,13 @@ public Task DeleteAsync(string tokenValue) }, saveChanges: true); } - public Task> GetAllAsync( + public Task> GetAllAsync( string userId) { - return UpdateTokenMapAsync(userId, tokenMap => - { - var result = tokenMap.ToDictionary( - entry => entry.Value.Id, - entry => entry.Value); - - return (IDictionary)result; - }, saveChanges: false); + return UpdateTokenMapAsync( + userId, + tokenMap => (IReadOnlyDictionary)tokenMap, + saveChanges: false); } private async Task UpdateTokenMapAsync( diff --git a/tests/Nexus.Tests/Services/TokenServiceTests.cs b/tests/Nexus.Tests/Services/TokenServiceTests.cs index 378caff7..60f52634 100644 --- a/tests/Nexus.Tests/Services/TokenServiceTests.cs +++ b/tests/Nexus.Tests/Services/TokenServiceTests.cs @@ -165,7 +165,7 @@ public async Task CanGetAllTokens() var actualTokenMap = await tokenService.GetAllAsync(string.Empty); // Assert - var expected = JsonSerializerHelper.SerializeIndented(expectedTokenMap.ToDictionary(entry => entry.Value.Id, entry => entry.Value)); + var expected = JsonSerializerHelper.SerializeIndented(expectedTokenMap); var actual = JsonSerializerHelper.SerializeIndented(actualTokenMap); Assert.Equal(expected, actual); From 438c6bdb8ebc53376db627e96bdb016b04d57222 Mon Sep 17 00:00:00 2001 From: Apollo3zehn Date: Thu, 7 Mar 2024 19:05:00 +0100 Subject: [PATCH 06/19] Generated client --- openapi.json | 224 ++++++++--------- src/clients/dotnet-client/NexusClient.g.cs | 218 ++++++++--------- .../python-client/nexus_api/_nexus_api.py | 225 +++++++++--------- 3 files changed, 302 insertions(+), 365 deletions(-) diff --git a/openapi.json b/openapi.json index 56d74ebe..e9d40c0b 100644 --- a/openapi.json +++ b/openapi.json @@ -1250,60 +1250,25 @@ } } }, - "/api/v1/users/tokens/refresh": { - "post": { + "/api/v1/users/tokens/delete": { + "delete": { "tags": [ "Users" ], - "summary": "Refreshes the JWT token.", - "operationId": "Users_RefreshToken", - "requestBody": { - "x-name": "request", - "description": "The refresh token request.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RefreshTokenRequest" - } - } - }, - "required": true, - "x-position": 1 - }, - "responses": { - "200": { - "description": "A new pair of JWT and refresh token.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TokenPair" - } - } - } + "summary": "Deletes a personal access token.", + "operationId": "Users_DeleteTokenByValue", + "parameters": [ + { + "name": "value", + "in": "query", + "required": true, + "description": "The personal access token to delete.", + "schema": { + "type": "string" + }, + "x-position": 1 } - } - } - }, - "/api/v1/users/tokens/revoke": { - "post": { - "tags": [ - "Users" ], - "summary": "Revokes a refresh token.", - "operationId": "Users_RevokeToken", - "requestBody": { - "x-name": "request", - "description": "The revoke token request.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RevokeTokenRequest" - } - } - }, - "required": true, - "x-position": 1 - }, "responses": { "200": { "description": "", @@ -1340,24 +1305,14 @@ } } }, - "/api/v1/users/tokens/generate": { + "/api/v1/users/tokens/create": { "post": { "tags": [ "Users" ], - "summary": "Generates a refresh token.", - "operationId": "Users_GenerateRefreshToken", + "summary": "Creates a personal access token.", + "operationId": "Users_CreateToken", "parameters": [ - { - "name": "description", - "in": "query", - "required": true, - "description": "The refresh token description.", - "schema": { - "type": "string" - }, - "x-position": 1 - }, { "name": "userId", "in": "query", @@ -1369,6 +1324,19 @@ "x-position": 2 } ], + "requestBody": { + "x-name": "request", + "description": "The create token request.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateTokenRequest" + } + } + }, + "required": true, + "x-position": 1 + }, "responses": { "200": { "description": "", @@ -1383,21 +1351,22 @@ } } }, - "/api/v1/users/accept-license": { - "get": { + "/api/v1/users/tokens/{tokenId}": { + "delete": { "tags": [ "Users" ], - "summary": "Accepts the license of the specified catalog.", - "operationId": "Users_AcceptLicense", + "summary": "Deletes a personal access token.", + "operationId": "Users_DeleteToken", "parameters": [ { - "name": "catalogId", - "in": "query", + "name": "tokenId", + "in": "path", "required": true, - "description": "The catalog identifier.", + "description": "The identifier of the personal access token.", "schema": { - "type": "string" + "type": "string", + "format": "guid" }, "x-position": 1 } @@ -1417,22 +1386,21 @@ } } }, - "/api/v1/users/tokens/{tokenId}": { - "delete": { + "/api/v1/users/accept-license": { + "get": { "tags": [ "Users" ], - "summary": "Deletes a refresh token.", - "operationId": "Users_DeleteRefreshToken", + "summary": "Accepts the license of the specified catalog.", + "operationId": "Users_AcceptLicense", "parameters": [ { - "name": "tokenId", - "in": "path", + "name": "catalogId", + "in": "query", "required": true, - "description": "The identifier of the refresh token.", + "description": "The catalog identifier.", "schema": { - "type": "string", - "format": "guid" + "type": "string" }, "x-position": 1 } @@ -1663,8 +1631,8 @@ "tags": [ "Users" ], - "summary": "Gets all refresh tokens.", - "operationId": "Users_GetRefreshTokens", + "summary": "Gets all personal access tokens.", + "operationId": "Users_GetTokens", "parameters": [ { "name": "userId", @@ -1685,7 +1653,7 @@ "schema": { "type": "object", "additionalProperties": { - "$ref": "#/components/schemas/RefreshToken" + "$ref": "#/components/schemas/PersonalAccessToken" } } } @@ -2224,43 +2192,6 @@ } } }, - "TokenPair": { - "type": "object", - "description": "A token pair.", - "additionalProperties": false, - "properties": { - "accessToken": { - "type": "string", - "description": "The JWT token." - }, - "refreshToken": { - "type": "string", - "description": "The refresh token." - } - } - }, - "RefreshTokenRequest": { - "type": "object", - "description": "A refresh token request.", - "additionalProperties": false, - "properties": { - "refreshToken": { - "type": "string", - "description": "The refresh token." - } - } - }, - "RevokeTokenRequest": { - "type": "object", - "description": "A revoke token request.", - "additionalProperties": false, - "properties": { - "refreshToken": { - "type": "string", - "description": "The refresh token." - } - } - }, "MeResponse": { "type": "object", "description": "A me response.", @@ -2282,11 +2213,11 @@ "type": "boolean", "description": "A boolean which indicates if the user is an administrator." }, - "refreshTokens": { + "personalAccessTokens": { "type": "object", - "description": "A list of currently present refresh tokens.", + "description": "A list of personal access tokens.", "additionalProperties": { - "$ref": "#/components/schemas/RefreshToken" + "$ref": "#/components/schemas/PersonalAccessToken" } } } @@ -2302,19 +2233,64 @@ } } }, - "RefreshToken": { + "PersonalAccessToken": { "type": "object", - "description": "A refresh token.", + "description": "A personal access token.", "additionalProperties": false, "properties": { + "description": { + "type": "string", + "description": "The token description." + }, "expires": { "type": "string", "description": "The date/time when the token expires.", "format": "date-time" }, + "claims": { + "type": "array", + "description": "The claims that will be part of the token.", + "items": { + "$ref": "#/components/schemas/TokenClaim" + } + } + } + }, + "TokenClaim": { + "type": "object", + "description": "A revoke token request.", + "additionalProperties": false, + "properties": { + "type": { + "type": "string", + "description": "The claim type." + }, + "value": { + "type": "string", + "description": "The claim value." + } + } + }, + "CreateTokenRequest": { + "type": "object", + "description": "A revoke token request.", + "additionalProperties": false, + "properties": { "description": { "type": "string", "description": "The token description." + }, + "expires": { + "type": "string", + "description": "The date/time when the token expires.", + "format": "date-time" + }, + "claims": { + "type": "array", + "description": "The claims that will be part of the token.", + "items": { + "$ref": "#/components/schemas/TokenClaim" + } } } }, diff --git a/src/clients/dotnet-client/NexusClient.g.cs b/src/clients/dotnet-client/NexusClient.g.cs index 87165546..b3fe2ef6 100644 --- a/src/clients/dotnet-client/NexusClient.g.cs +++ b/src/clients/dotnet-client/NexusClient.g.cs @@ -2410,30 +2410,17 @@ public interface IUsersClient Task SignOutAsync(string returnUrl, CancellationToken cancellationToken = default); /// - /// Refreshes the JWT token. + /// Deletes a personal access token. /// - /// The refresh token request. - TokenPair RefreshToken(RefreshTokenRequest request); + /// The personal access token to delete. + HttpResponseMessage DeleteTokenByValue(string value); /// - /// Refreshes the JWT token. + /// Deletes a personal access token. /// - /// The refresh token request. + /// The personal access token to delete. /// The token to cancel the current operation. - Task RefreshTokenAsync(RefreshTokenRequest request, CancellationToken cancellationToken = default); - - /// - /// Revokes a refresh token. - /// - /// The revoke token request. - HttpResponseMessage RevokeToken(RevokeTokenRequest request); - - /// - /// Revokes a refresh token. - /// - /// The revoke token request. - /// The token to cancel the current operation. - Task RevokeTokenAsync(RevokeTokenRequest request, CancellationToken cancellationToken = default); + Task DeleteTokenByValueAsync(string value, CancellationToken cancellationToken = default); /// /// Gets the current user. @@ -2447,45 +2434,45 @@ public interface IUsersClient Task GetMeAsync(CancellationToken cancellationToken = default); /// - /// Generates a refresh token. + /// Creates a personal access token. /// - /// The refresh token description. /// The optional user identifier. If not specified, the current user will be used. - string GenerateRefreshToken(string description, string? userId = default); + /// The create token request. + string CreateToken(CreateTokenRequest request, string? userId = default); /// - /// Generates a refresh token. + /// Creates a personal access token. /// - /// The refresh token description. /// The optional user identifier. If not specified, the current user will be used. + /// The create token request. /// The token to cancel the current operation. - Task GenerateRefreshTokenAsync(string description, string? userId = default, CancellationToken cancellationToken = default); + Task CreateTokenAsync(CreateTokenRequest request, string? userId = default, CancellationToken cancellationToken = default); /// - /// Accepts the license of the specified catalog. + /// Deletes a personal access token. /// - /// The catalog identifier. - HttpResponseMessage AcceptLicense(string catalogId); + /// The identifier of the personal access token. + HttpResponseMessage DeleteToken(Guid tokenId); /// - /// Accepts the license of the specified catalog. + /// Deletes a personal access token. /// - /// The catalog identifier. + /// The identifier of the personal access token. /// The token to cancel the current operation. - Task AcceptLicenseAsync(string catalogId, CancellationToken cancellationToken = default); + Task DeleteTokenAsync(Guid tokenId, CancellationToken cancellationToken = default); /// - /// Deletes a refresh token. + /// Accepts the license of the specified catalog. /// - /// The identifier of the refresh token. - HttpResponseMessage DeleteRefreshToken(Guid tokenId); + /// The catalog identifier. + HttpResponseMessage AcceptLicense(string catalogId); /// - /// Deletes a refresh token. + /// Accepts the license of the specified catalog. /// - /// The identifier of the refresh token. + /// The catalog identifier. /// The token to cancel the current operation. - Task DeleteRefreshTokenAsync(Guid tokenId, CancellationToken cancellationToken = default); + Task AcceptLicenseAsync(string catalogId, CancellationToken cancellationToken = default); /// /// Gets a list of users. @@ -2566,17 +2553,17 @@ public interface IUsersClient Task DeleteClaimAsync(Guid claimId, CancellationToken cancellationToken = default); /// - /// Gets all refresh tokens. + /// Gets all personal access tokens. /// /// The identifier of the user. - IReadOnlyDictionary GetRefreshTokens(string userId); + IReadOnlyDictionary GetTokens(string userId); /// - /// Gets all refresh tokens. + /// Gets all personal access tokens. /// /// The identifier of the user. /// The token to cancel the current operation. - Task> GetRefreshTokensAsync(string userId, CancellationToken cancellationToken = default); + Task> GetTokensAsync(string userId, CancellationToken cancellationToken = default); } @@ -2683,43 +2670,37 @@ public Task SignOutAsync(string returnUrl, CancellationToken cancellationToken = } /// - public TokenPair RefreshToken(RefreshTokenRequest request) + public HttpResponseMessage DeleteTokenByValue(string value) { var __urlBuilder = new StringBuilder(); - __urlBuilder.Append("/api/v1/users/tokens/refresh"); + __urlBuilder.Append("/api/v1/users/tokens/delete"); - var __url = __urlBuilder.ToString(); - return ___client.Invoke("POST", __url, "application/json", "application/json", JsonContent.Create(request, options: Utilities.JsonOptions)); - } + var __queryValues = new Dictionary(); - /// - public Task RefreshTokenAsync(RefreshTokenRequest request, CancellationToken cancellationToken = default) - { - var __urlBuilder = new StringBuilder(); - __urlBuilder.Append("/api/v1/users/tokens/refresh"); + __queryValues["value"] = Uri.EscapeDataString(value); + + var __query = "?" + string.Join('&', __queryValues.Select(entry => $"{entry.Key}={entry.Value}")); + __urlBuilder.Append(__query); var __url = __urlBuilder.ToString(); - return ___client.InvokeAsync("POST", __url, "application/json", "application/json", JsonContent.Create(request, options: Utilities.JsonOptions), cancellationToken); + return ___client.Invoke("DELETE", __url, "application/octet-stream", default, default); } /// - public HttpResponseMessage RevokeToken(RevokeTokenRequest request) + public Task DeleteTokenByValueAsync(string value, CancellationToken cancellationToken = default) { var __urlBuilder = new StringBuilder(); - __urlBuilder.Append("/api/v1/users/tokens/revoke"); + __urlBuilder.Append("/api/v1/users/tokens/delete"); - var __url = __urlBuilder.ToString(); - return ___client.Invoke("POST", __url, "application/octet-stream", "application/json", JsonContent.Create(request, options: Utilities.JsonOptions)); - } + var __queryValues = new Dictionary(); - /// - public Task RevokeTokenAsync(RevokeTokenRequest request, CancellationToken cancellationToken = default) - { - var __urlBuilder = new StringBuilder(); - __urlBuilder.Append("/api/v1/users/tokens/revoke"); + __queryValues["value"] = Uri.EscapeDataString(value); + + var __query = "?" + string.Join('&', __queryValues.Select(entry => $"{entry.Key}={entry.Value}")); + __urlBuilder.Append(__query); var __url = __urlBuilder.ToString(); - return ___client.InvokeAsync("POST", __url, "application/octet-stream", "application/json", JsonContent.Create(request, options: Utilities.JsonOptions), cancellationToken); + return ___client.InvokeAsync("DELETE", __url, "application/octet-stream", default, default, cancellationToken); } /// @@ -2743,15 +2724,13 @@ public Task GetMeAsync(CancellationToken cancellationToken = default } /// - public string GenerateRefreshToken(string description, string? userId = default) + public string CreateToken(CreateTokenRequest request, string? userId = default) { var __urlBuilder = new StringBuilder(); - __urlBuilder.Append("/api/v1/users/tokens/generate"); + __urlBuilder.Append("/api/v1/users/tokens/create"); var __queryValues = new Dictionary(); - __queryValues["description"] = Uri.EscapeDataString(description); - if (userId is not null) __queryValues["userId"] = Uri.EscapeDataString(Convert.ToString(userId, CultureInfo.InvariantCulture)!); @@ -2759,19 +2738,17 @@ public string GenerateRefreshToken(string description, string? userId = default) __urlBuilder.Append(__query); var __url = __urlBuilder.ToString(); - return ___client.Invoke("POST", __url, "application/json", default, default); + return ___client.Invoke("POST", __url, "application/json", "application/json", JsonContent.Create(request, options: Utilities.JsonOptions)); } /// - public Task GenerateRefreshTokenAsync(string description, string? userId = default, CancellationToken cancellationToken = default) + public Task CreateTokenAsync(CreateTokenRequest request, string? userId = default, CancellationToken cancellationToken = default) { var __urlBuilder = new StringBuilder(); - __urlBuilder.Append("/api/v1/users/tokens/generate"); + __urlBuilder.Append("/api/v1/users/tokens/create"); var __queryValues = new Dictionary(); - __queryValues["description"] = Uri.EscapeDataString(description); - if (userId is not null) __queryValues["userId"] = Uri.EscapeDataString(Convert.ToString(userId, CultureInfo.InvariantCulture)!); @@ -2779,7 +2756,29 @@ public Task GenerateRefreshTokenAsync(string description, string? userId __urlBuilder.Append(__query); var __url = __urlBuilder.ToString(); - return ___client.InvokeAsync("POST", __url, "application/json", default, default, cancellationToken); + return ___client.InvokeAsync("POST", __url, "application/json", "application/json", JsonContent.Create(request, options: Utilities.JsonOptions), cancellationToken); + } + + /// + public HttpResponseMessage DeleteToken(Guid tokenId) + { + var __urlBuilder = new StringBuilder(); + __urlBuilder.Append("/api/v1/users/tokens/{tokenId}"); + __urlBuilder.Replace("{tokenId}", Uri.EscapeDataString(Convert.ToString(tokenId, CultureInfo.InvariantCulture)!)); + + var __url = __urlBuilder.ToString(); + return ___client.Invoke("DELETE", __url, "application/octet-stream", default, default); + } + + /// + public Task DeleteTokenAsync(Guid tokenId, CancellationToken cancellationToken = default) + { + var __urlBuilder = new StringBuilder(); + __urlBuilder.Append("/api/v1/users/tokens/{tokenId}"); + __urlBuilder.Replace("{tokenId}", Uri.EscapeDataString(Convert.ToString(tokenId, CultureInfo.InvariantCulture)!)); + + var __url = __urlBuilder.ToString(); + return ___client.InvokeAsync("DELETE", __url, "application/octet-stream", default, default, cancellationToken); } /// @@ -2816,28 +2815,6 @@ public Task AcceptLicenseAsync(string catalogId, Cancellati return ___client.InvokeAsync("GET", __url, "application/octet-stream", default, default, cancellationToken); } - /// - public HttpResponseMessage DeleteRefreshToken(Guid tokenId) - { - var __urlBuilder = new StringBuilder(); - __urlBuilder.Append("/api/v1/users/tokens/{tokenId}"); - __urlBuilder.Replace("{tokenId}", Uri.EscapeDataString(Convert.ToString(tokenId, CultureInfo.InvariantCulture)!)); - - var __url = __urlBuilder.ToString(); - return ___client.Invoke("DELETE", __url, "application/octet-stream", default, default); - } - - /// - public Task DeleteRefreshTokenAsync(Guid tokenId, CancellationToken cancellationToken = default) - { - var __urlBuilder = new StringBuilder(); - __urlBuilder.Append("/api/v1/users/tokens/{tokenId}"); - __urlBuilder.Replace("{tokenId}", Uri.EscapeDataString(Convert.ToString(tokenId, CultureInfo.InvariantCulture)!)); - - var __url = __urlBuilder.ToString(); - return ___client.InvokeAsync("DELETE", __url, "application/octet-stream", default, default, cancellationToken); - } - /// public IReadOnlyDictionary GetUsers() { @@ -2967,25 +2944,25 @@ public Task DeleteClaimAsync(Guid claimId, CancellationToke } /// - public IReadOnlyDictionary GetRefreshTokens(string userId) + public IReadOnlyDictionary GetTokens(string userId) { var __urlBuilder = new StringBuilder(); __urlBuilder.Append("/api/v1/users/{userId}/tokens"); __urlBuilder.Replace("{userId}", Uri.EscapeDataString(userId)); var __url = __urlBuilder.ToString(); - return ___client.Invoke>("GET", __url, "application/json", default, default); + return ___client.Invoke>("GET", __url, "application/json", default, default); } /// - public Task> GetRefreshTokensAsync(string userId, CancellationToken cancellationToken = default) + public Task> GetTokensAsync(string userId, CancellationToken cancellationToken = default) { var __urlBuilder = new StringBuilder(); __urlBuilder.Append("/api/v1/users/{userId}/tokens"); __urlBuilder.Replace("{userId}", Uri.EscapeDataString(userId)); var __url = __urlBuilder.ToString(); - return ___client.InvokeAsync>("GET", __url, "application/json", default, default, cancellationToken); + return ___client.InvokeAsync>("GET", __url, "application/json", default, default, cancellationToken); } } @@ -3341,33 +3318,14 @@ public record DataSourceRegistration(string Type, Uri? ResourceLocator, IReadOnl /// The display name. public record AuthenticationSchemeDescription(string Scheme, string DisplayName); -/// -/// A token pair. -/// -/// The JWT token. -/// The refresh token. -public record TokenPair(string AccessToken, string RefreshToken); - -/// -/// A refresh token request. -/// -/// The refresh token. -public record RefreshTokenRequest(string RefreshToken); - -/// -/// A revoke token request. -/// -/// The refresh token. -public record RevokeTokenRequest(string RefreshToken); - /// /// A me response. /// /// The user id. /// The user. /// A boolean which indicates if the user is an administrator. -/// A list of currently present refresh tokens. -public record MeResponse(string UserId, NexusUser User, bool IsAdmin, IReadOnlyDictionary RefreshTokens); +/// A list of personal access tokens. +public record MeResponse(string UserId, NexusUser User, bool IsAdmin, IReadOnlyDictionary PersonalAccessTokens); /// /// Represents a user. @@ -3376,11 +3334,27 @@ public record MeResponse(string UserId, NexusUser User, bool IsAdmin, IReadOnlyD public record NexusUser(string Name); /// -/// A refresh token. +/// 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); + +/// +/// A revoke token request. +/// /// The token description. -public record RefreshToken(DateTime Expires, string Description); +/// The date/time when the token expires. +/// The claims that will be part of the token. +public record CreateTokenRequest(string Description, DateTime Expires, IReadOnlyList Claims); /// /// Represents a claim. diff --git a/src/clients/python-client/nexus_api/_nexus_api.py b/src/clients/python-client/nexus_api/_nexus_api.py index db69a417..130f63fd 100644 --- a/src/clients/python-client/nexus_api/_nexus_api.py +++ b/src/clients/python-client/nexus_api/_nexus_api.py @@ -718,49 +718,6 @@ class AuthenticationSchemeDescription: """The display name.""" -@dataclass(frozen=True) -class TokenPair: - """ - A token pair. - - Args: - access_token: The JWT token. - refresh_token: The refresh token. - """ - - access_token: str - """The JWT token.""" - - refresh_token: str - """The refresh token.""" - - -@dataclass(frozen=True) -class RefreshTokenRequest: - """ - A refresh token request. - - Args: - refresh_token: The refresh token. - """ - - refresh_token: str - """The refresh token.""" - - -@dataclass(frozen=True) -class RevokeTokenRequest: - """ - A revoke token request. - - Args: - refresh_token: The refresh token. - """ - - refresh_token: str - """The refresh token.""" - - @dataclass(frozen=True) class MeResponse: """ @@ -770,7 +727,7 @@ class MeResponse: user_id: The user id. user: The user. is_admin: A boolean which indicates if the user is an administrator. - refresh_tokens: A list of currently present refresh tokens. + personal_access_tokens: A list of personal access tokens. """ user_id: str @@ -782,8 +739,8 @@ class MeResponse: is_admin: bool """A boolean which indicates if the user is an administrator.""" - refresh_tokens: dict[str, RefreshToken] - """A list of currently present refresh tokens.""" + personal_access_tokens: dict[str, PersonalAccessToken] + """A list of personal access tokens.""" @dataclass(frozen=True) @@ -800,21 +757,63 @@ class NexusUser: @dataclass(frozen=True) -class RefreshToken: +class PersonalAccessToken: """ - A refresh token. + A personal access token. Args: - expires: The date/time when the token expires. description: The token description. + expires: The date/time when the token expires. + claims: The claims that will be part of the token. """ + description: str + """The token description.""" + expires: datetime """The date/time when the token expires.""" + claims: list[TokenClaim] + """The claims that will be part of the token.""" + + +@dataclass(frozen=True) +class TokenClaim: + """ + A revoke token request. + + Args: + type: The claim type. + value: The claim value. + """ + + type: str + """The claim type.""" + + value: str + """The claim value.""" + + +@dataclass(frozen=True) +class CreateTokenRequest: + """ + A revoke token request. + + Args: + description: The token description. + expires: The date/time when the token expires. + claims: The claims that will be part of the token. + """ + description: str """The token description.""" + expires: datetime + """The date/time when the token expires.""" + + claims: list[TokenClaim] + """The claims that will be part of the token.""" + @dataclass(frozen=True) class NexusClaim: @@ -1418,27 +1417,24 @@ def sign_out(self, return_url: str) -> Awaitable[None]: return self.___client._invoke(type(None), "POST", __url, None, None, None) - def refresh_token(self, request: RefreshTokenRequest) -> Awaitable[TokenPair]: + def delete_token_by_value(self, value: str) -> Awaitable[Response]: """ - Refreshes the JWT token. + Deletes a personal access token. Args: + value: The personal access token to delete. """ - __url = "/api/v1/users/tokens/refresh" + __url = "/api/v1/users/tokens/delete" - return self.___client._invoke(TokenPair, "POST", __url, "application/json", "application/json", json.dumps(JsonEncoder.encode(request, _json_encoder_options))) - - def revoke_token(self, request: RevokeTokenRequest) -> Awaitable[Response]: - """ - Revokes a refresh token. + __query_values: dict[str, str] = {} - Args: - """ + __query_values["value"] = quote(_to_string(value), safe="") - __url = "/api/v1/users/tokens/revoke" + __query: str = "?" + "&".join(f"{key}={value}" for (key, value) in __query_values.items()) + __url += __query - return self.___client._invoke(Response, "POST", __url, "application/octet-stream", "application/json", json.dumps(JsonEncoder.encode(request, _json_encoder_options))) + return self.___client._invoke(Response, "DELETE", __url, "application/octet-stream", None, None) def get_me(self) -> Awaitable[MeResponse]: """ @@ -1451,28 +1447,38 @@ def get_me(self) -> Awaitable[MeResponse]: return self.___client._invoke(MeResponse, "GET", __url, "application/json", None, None) - def generate_refresh_token(self, description: str, user_id: Optional[str] = None) -> Awaitable[str]: + def create_token(self, request: CreateTokenRequest, user_id: Optional[str] = None) -> Awaitable[str]: """ - Generates a refresh token. + Creates a personal access token. Args: - description: The refresh token description. user_id: The optional user identifier. If not specified, the current user will be used. """ - __url = "/api/v1/users/tokens/generate" + __url = "/api/v1/users/tokens/create" __query_values: dict[str, str] = {} - __query_values["description"] = quote(_to_string(description), safe="") - if user_id is not None: __query_values["userId"] = quote(_to_string(user_id), safe="") __query: str = "?" + "&".join(f"{key}={value}" for (key, value) in __query_values.items()) __url += __query - return self.___client._invoke(str, "POST", __url, "application/json", None, None) + return self.___client._invoke(str, "POST", __url, "application/json", "application/json", json.dumps(JsonEncoder.encode(request, _json_encoder_options))) + + def delete_token(self, token_id: UUID) -> Awaitable[Response]: + """ + Deletes a personal access token. + + Args: + token_id: The identifier of the personal access token. + """ + + __url = "/api/v1/users/tokens/{tokenId}" + __url = __url.replace("{tokenId}", quote(str(token_id), safe="")) + + return self.___client._invoke(Response, "DELETE", __url, "application/octet-stream", None, None) def accept_license(self, catalog_id: str) -> Awaitable[Response]: """ @@ -1493,19 +1499,6 @@ def accept_license(self, catalog_id: str) -> Awaitable[Response]: return self.___client._invoke(Response, "GET", __url, "application/octet-stream", None, None) - def delete_refresh_token(self, token_id: UUID) -> Awaitable[Response]: - """ - Deletes a refresh token. - - Args: - token_id: The identifier of the refresh token. - """ - - __url = "/api/v1/users/tokens/{tokenId}" - __url = __url.replace("{tokenId}", quote(str(token_id), safe="")) - - return self.___client._invoke(Response, "DELETE", __url, "application/octet-stream", None, None) - def get_users(self) -> Awaitable[dict[str, NexusUser]]: """ Gets a list of users. @@ -1580,9 +1573,9 @@ def delete_claim(self, claim_id: UUID) -> Awaitable[Response]: return self.___client._invoke(Response, "DELETE", __url, "application/octet-stream", None, None) - def get_refresh_tokens(self, user_id: str) -> Awaitable[dict[str, RefreshToken]]: + def get_tokens(self, user_id: str) -> Awaitable[dict[str, PersonalAccessToken]]: """ - Gets all refresh tokens. + Gets all personal access tokens. Args: user_id: The identifier of the user. @@ -1591,7 +1584,7 @@ def get_refresh_tokens(self, user_id: str) -> Awaitable[dict[str, RefreshToken]] __url = "/api/v1/users/{userId}/tokens" __url = __url.replace("{userId}", quote(str(user_id), safe="")) - return self.___client._invoke(dict[str, RefreshToken], "GET", __url, "application/json", None, None) + return self.___client._invoke(dict[str, PersonalAccessToken], "GET", __url, "application/json", None, None) class WritersAsyncClient: @@ -2199,27 +2192,24 @@ def sign_out(self, return_url: str) -> None: return self.___client._invoke(type(None), "POST", __url, None, None, None) - def refresh_token(self, request: RefreshTokenRequest) -> TokenPair: + def delete_token_by_value(self, value: str) -> Response: """ - Refreshes the JWT token. + Deletes a personal access token. Args: + value: The personal access token to delete. """ - __url = "/api/v1/users/tokens/refresh" - - return self.___client._invoke(TokenPair, "POST", __url, "application/json", "application/json", json.dumps(JsonEncoder.encode(request, _json_encoder_options))) + __url = "/api/v1/users/tokens/delete" - def revoke_token(self, request: RevokeTokenRequest) -> Response: - """ - Revokes a refresh token. + __query_values: dict[str, str] = {} - Args: - """ + __query_values["value"] = quote(_to_string(value), safe="") - __url = "/api/v1/users/tokens/revoke" + __query: str = "?" + "&".join(f"{key}={value}" for (key, value) in __query_values.items()) + __url += __query - return self.___client._invoke(Response, "POST", __url, "application/octet-stream", "application/json", json.dumps(JsonEncoder.encode(request, _json_encoder_options))) + return self.___client._invoke(Response, "DELETE", __url, "application/octet-stream", None, None) def get_me(self) -> MeResponse: """ @@ -2232,28 +2222,38 @@ def get_me(self) -> MeResponse: return self.___client._invoke(MeResponse, "GET", __url, "application/json", None, None) - def generate_refresh_token(self, description: str, user_id: Optional[str] = None) -> str: + def create_token(self, request: CreateTokenRequest, user_id: Optional[str] = None) -> str: """ - Generates a refresh token. + Creates a personal access token. Args: - description: The refresh token description. user_id: The optional user identifier. If not specified, the current user will be used. """ - __url = "/api/v1/users/tokens/generate" + __url = "/api/v1/users/tokens/create" __query_values: dict[str, str] = {} - __query_values["description"] = quote(_to_string(description), safe="") - if user_id is not None: __query_values["userId"] = quote(_to_string(user_id), safe="") __query: str = "?" + "&".join(f"{key}={value}" for (key, value) in __query_values.items()) __url += __query - return self.___client._invoke(str, "POST", __url, "application/json", None, None) + return self.___client._invoke(str, "POST", __url, "application/json", "application/json", json.dumps(JsonEncoder.encode(request, _json_encoder_options))) + + def delete_token(self, token_id: UUID) -> Response: + """ + Deletes a personal access token. + + Args: + token_id: The identifier of the personal access token. + """ + + __url = "/api/v1/users/tokens/{tokenId}" + __url = __url.replace("{tokenId}", quote(str(token_id), safe="")) + + return self.___client._invoke(Response, "DELETE", __url, "application/octet-stream", None, None) def accept_license(self, catalog_id: str) -> Response: """ @@ -2274,19 +2274,6 @@ def accept_license(self, catalog_id: str) -> Response: return self.___client._invoke(Response, "GET", __url, "application/octet-stream", None, None) - def delete_refresh_token(self, token_id: UUID) -> Response: - """ - Deletes a refresh token. - - Args: - token_id: The identifier of the refresh token. - """ - - __url = "/api/v1/users/tokens/{tokenId}" - __url = __url.replace("{tokenId}", quote(str(token_id), safe="")) - - return self.___client._invoke(Response, "DELETE", __url, "application/octet-stream", None, None) - def get_users(self) -> dict[str, NexusUser]: """ Gets a list of users. @@ -2361,9 +2348,9 @@ def delete_claim(self, claim_id: UUID) -> Response: return self.___client._invoke(Response, "DELETE", __url, "application/octet-stream", None, None) - def get_refresh_tokens(self, user_id: str) -> dict[str, RefreshToken]: + def get_tokens(self, user_id: str) -> dict[str, PersonalAccessToken]: """ - Gets all refresh tokens. + Gets all personal access tokens. Args: user_id: The identifier of the user. @@ -2372,7 +2359,7 @@ def get_refresh_tokens(self, user_id: str) -> dict[str, RefreshToken]: __url = "/api/v1/users/{userId}/tokens" __url = __url.replace("{userId}", quote(str(user_id), safe="")) - return self.___client._invoke(dict[str, RefreshToken], "GET", __url, "application/json", None, None) + return self.___client._invoke(dict[str, PersonalAccessToken], "GET", __url, "application/json", None, None) class WritersClient: From c584c1a13f80af57d325512908ae83f77d29debc Mon Sep 17 00:00:00 2001 From: Apollo3zehn Date: Thu, 7 Mar 2024 19:05:00 +0100 Subject: [PATCH 07/19] Manual changes --- .../Nexus.ClientGenerator.csproj | 2 +- src/clients/dotnet-client/NexusClient.g.cs | 248 ++---------------- .../python-client/nexus_api/_nexus_api.py | 231 +++------------- 3 files changed, 54 insertions(+), 427 deletions(-) diff --git a/src/Nexus.ClientGenerator/Nexus.ClientGenerator.csproj b/src/Nexus.ClientGenerator/Nexus.ClientGenerator.csproj index 7a30a843..63d3a96b 100644 --- a/src/Nexus.ClientGenerator/Nexus.ClientGenerator.csproj +++ b/src/Nexus.ClientGenerator/Nexus.ClientGenerator.csproj @@ -11,7 +11,7 @@ - + \ No newline at end of file diff --git a/src/clients/dotnet-client/NexusClient.g.cs b/src/clients/dotnet-client/NexusClient.g.cs index b3fe2ef6..cb21e0f9 100644 --- a/src/clients/dotnet-client/NexusClient.g.cs +++ b/src/clients/dotnet-client/NexusClient.g.cs @@ -4,11 +4,9 @@ using System.Diagnostics; using System.Globalization; using System.IO.Compression; -using System.Net; using System.Net.Http.Headers; using System.Net.Http.Json; using System.Runtime.InteropServices; -using System.Security.Cryptography; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; @@ -70,17 +68,9 @@ public interface INexusClient /// /// Signs in the user. /// - /// The refresh token. + /// The access token. /// A task. - void SignIn(string refreshToken); - - /// - /// Signs in the user. - /// - /// The refresh token. - /// A token to cancel the current operation. - /// A task. - Task SignInAsync(string refreshToken, CancellationToken cancellationToken); + void SignIn(string accessToken); /// /// Attaches configuration data to subsequent API requests. @@ -100,11 +90,7 @@ public class NexusClient : INexusClient, IDisposable private const string ConfigurationHeaderKey = "Nexus-Configuration"; private const string AuthorizationHeaderKey = "Authorization"; - private static string _tokenFolderPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "", "tokens"); - private static SemaphoreSlim _semaphoreSlim = new SemaphoreSlim(initialCount: 1, maxCount: 1); - - private TokenPair? _tokenPair; - private string? _tokenFilePath; + private string? _token; private HttpClient _httpClient; private ArtifactsClient _artifacts; @@ -152,7 +138,7 @@ public NexusClient(HttpClient httpClient) /// /// Gets a value which indicates if the user is authenticated. /// - public bool IsAuthenticated => _tokenPair is not null; + public bool IsAuthenticated => _token is not null; /// public IArtifactsClient Artifacts => _artifacts; @@ -184,51 +170,13 @@ public NexusClient(HttpClient httpClient) /// - public void SignIn(string refreshToken) - { - string actualRefreshToken; - - var byteHash = SHA256.HashData(Encoding.UTF8.GetBytes(refreshToken)); - var refreshTokenHash = BitConverter.ToString(byteHash).Replace("-", ""); - _tokenFilePath = Path.Combine(_tokenFolderPath, refreshTokenHash + ".json"); - - if (File.Exists(_tokenFilePath)) - { - actualRefreshToken = File.ReadAllText(_tokenFilePath); - } - - else - { - Directory.CreateDirectory(_tokenFolderPath); - File.WriteAllText(_tokenFilePath, refreshToken); - actualRefreshToken = refreshToken; - } - - RefreshToken(actualRefreshToken); - } - - /// - public async Task SignInAsync(string refreshToken, CancellationToken cancellationToken = default) + public void SignIn(string accessToken) { - string actualRefreshToken; - - var byteHash = SHA256.HashData(Encoding.UTF8.GetBytes(refreshToken)); - var refreshTokenHash = BitConverter.ToString(byteHash).Replace("-", ""); - _tokenFilePath = Path.Combine(_tokenFolderPath, refreshTokenHash + ".json"); - - if (File.Exists(_tokenFilePath)) - { - actualRefreshToken = File.ReadAllText(_tokenFilePath); - } - - else - { - Directory.CreateDirectory(_tokenFolderPath); - File.WriteAllText(_tokenFilePath, refreshToken); - actualRefreshToken = refreshToken; - } + var authorizationHeaderValue = $"Bearer {accessToken}"; + _httpClient.DefaultRequestHeaders.Remove(AuthorizationHeaderKey); + _httpClient.DefaultRequestHeaders.Add(AuthorizationHeaderKey, authorizationHeaderValue); - await RefreshTokenAsync(actualRefreshToken, cancellationToken).ConfigureAwait(false); + _token = accessToken; } /// @@ -259,54 +207,14 @@ internal T Invoke(string method, string relativeUrl, string? acceptHeaderValu // process response if (!response.IsSuccessStatusCode) { - // try to refresh the access token - if (response.StatusCode == HttpStatusCode.Unauthorized && _tokenPair is not null) - { - var wwwAuthenticateHeader = response.Headers.WwwAuthenticate.FirstOrDefault(); - var signOut = true; + var message = new StreamReader(response.Content.ReadAsStream()).ReadToEnd(); + var statusCode = $"N00.{(int)response.StatusCode}"; - if (wwwAuthenticateHeader is not null) - { - var parameter = wwwAuthenticateHeader.Parameter; - - if (parameter is not null && parameter.Contains("The token expired at")) - { - try - { - RefreshToken(_tokenPair.RefreshToken); - - using var newRequest = BuildRequestMessage(method, relativeUrl, content, contentTypeValue, acceptHeaderValue); - var newResponse = _httpClient.Send(newRequest, HttpCompletionOption.ResponseHeadersRead); - - if (newResponse is not null) - { - response.Dispose(); - response = newResponse; - signOut = false; - } - } - catch - { - // - } - } - } - - if (signOut) - SignOut(); - } + if (string.IsNullOrWhiteSpace(message)) + throw new NexusException(statusCode, $"The HTTP request failed with status code {response.StatusCode}."); - if (!response.IsSuccessStatusCode) - { - var message = new StreamReader(response.Content.ReadAsStream()).ReadToEnd(); - var statusCode = $"N00.{(int)response.StatusCode}"; - - if (string.IsNullOrWhiteSpace(message)) - throw new NexusException(statusCode, $"The HTTP request failed with status code {response.StatusCode}."); - - else - throw new NexusException(statusCode, $"The HTTP request failed with status code {response.StatusCode}. The response message is: {message}"); - } + else + throw new NexusException(statusCode, $"The HTTP request failed with status code {response.StatusCode}. The response message is: {message}"); } try @@ -353,54 +261,14 @@ internal async Task InvokeAsync(string method, string relativeUrl, string? // process response if (!response.IsSuccessStatusCode) { - // try to refresh the access token - if (response.StatusCode == HttpStatusCode.Unauthorized && _tokenPair is not null) - { - var wwwAuthenticateHeader = response.Headers.WwwAuthenticate.FirstOrDefault(); - var signOut = true; + var message = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + var statusCode = $"N00.{(int)response.StatusCode}"; - if (wwwAuthenticateHeader is not null) - { - var parameter = wwwAuthenticateHeader.Parameter; + if (string.IsNullOrWhiteSpace(message)) + throw new NexusException(statusCode, $"The HTTP request failed with status code {response.StatusCode}."); - if (parameter is not null && parameter.Contains("The token expired at")) - { - try - { - await RefreshTokenAsync(_tokenPair.RefreshToken, cancellationToken).ConfigureAwait(false); - - using var newRequest = BuildRequestMessage(method, relativeUrl, content, contentTypeValue, acceptHeaderValue); - var newResponse = await _httpClient.SendAsync(newRequest, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); - - if (newResponse is not null) - { - response.Dispose(); - response = newResponse; - signOut = false; - } - } - catch - { - // - } - } - } - - if (signOut) - SignOut(); - } - - if (!response.IsSuccessStatusCode) - { - var message = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - var statusCode = $"N00.{(int)response.StatusCode}"; - - if (string.IsNullOrWhiteSpace(message)) - throw new NexusException(statusCode, $"The HTTP request failed with status code {response.StatusCode}."); - - else - throw new NexusException(statusCode, $"The HTTP request failed with status code {response.StatusCode}. The response message is: {message}"); - } + else + throw new NexusException(statusCode, $"The HTTP request failed with status code {response.StatusCode}. The response message is: {message}"); } try @@ -461,80 +329,6 @@ private HttpRequestMessage BuildRequestMessage(string method, string relativeUrl return requestMessage; } - private void RefreshToken(string refreshToken) - { - _semaphoreSlim.Wait(); - - try - { - // make sure the refresh token has not already been redeemed - if (_tokenPair is not null && refreshToken != _tokenPair.RefreshToken) - return; - - // see https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/blob/dev/src/Microsoft.IdentityModel.Tokens/Validators.cs#L390 - - var refreshRequest = new RefreshTokenRequest(refreshToken); - var tokenPair = Users.RefreshToken(refreshRequest); - - if (_tokenFilePath is not null) - { - Directory.CreateDirectory(_tokenFolderPath); - File.WriteAllText(_tokenFilePath, tokenPair.RefreshToken); - } - - var authorizationHeaderValue = $"Bearer {tokenPair.AccessToken}"; - _httpClient.DefaultRequestHeaders.Remove(AuthorizationHeaderKey); - _httpClient.DefaultRequestHeaders.Add(AuthorizationHeaderKey, authorizationHeaderValue); - - _tokenPair = tokenPair; - - } - finally - { - _semaphoreSlim.Release(); - } - } - - private async Task RefreshTokenAsync(string refreshToken, CancellationToken cancellationToken) - { - await _semaphoreSlim.WaitAsync().ConfigureAwait(false); - - try - { - // make sure the refresh token has not already been redeemed - if (_tokenPair is not null && refreshToken != _tokenPair.RefreshToken) - return; - - // see https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/blob/dev/src/Microsoft.IdentityModel.Tokens/Validators.cs#L390 - - var refreshRequest = new RefreshTokenRequest(refreshToken); - var tokenPair = await Users.RefreshTokenAsync(refreshRequest, cancellationToken).ConfigureAwait(false); - - if (_tokenFilePath is not null) - { - Directory.CreateDirectory(_tokenFolderPath); - File.WriteAllText(_tokenFilePath, tokenPair.RefreshToken); - } - - var authorizationHeaderValue = $"Bearer {tokenPair.AccessToken}"; - _httpClient.DefaultRequestHeaders.Remove(AuthorizationHeaderKey); - _httpClient.DefaultRequestHeaders.Add(AuthorizationHeaderKey, authorizationHeaderValue); - - _tokenPair = tokenPair; - - } - finally - { - _semaphoreSlim.Release(); - } - } - - private void SignOut() - { - _httpClient.DefaultRequestHeaders.Remove(AuthorizationHeaderKey); - _tokenPair = default; - } - /// public void Dispose() { diff --git a/src/clients/python-client/nexus_api/_nexus_api.py b/src/clients/python-client/nexus_api/_nexus_api.py index 130f63fd..367d2430 100644 --- a/src/clients/python-client/nexus_api/_nexus_api.py +++ b/src/clients/python-client/nexus_api/_nexus_api.py @@ -213,17 +213,13 @@ def to_snake_case(value: str) -> str: import asyncio import base64 -import hashlib import json -import os import time from array import array from dataclasses import dataclass from datetime import datetime, timedelta from enum import Enum -from pathlib import Path from tempfile import NamedTemporaryFile -from threading import Lock from typing import (Any, AsyncIterable, Awaitable, Callable, Iterable, Optional, Type, Union, cast) from urllib.parse import quote @@ -231,7 +227,6 @@ def to_snake_case(value: str) -> str: from zipfile import ZipFile from httpx import AsyncClient, Client, Request, Response -from httpx import codes def _to_string(value: Any) -> str: @@ -2435,11 +2430,7 @@ class NexusAsyncClient: _configuration_header_key: str = "Nexus-Configuration" _authorization_header_key: str = "Authorization" - _token_folder_path: str = os.path.join(str(Path.home()), "", "tokens") - _mutex: Lock = Lock() - - _token_pair: Optional[TokenPair] - _token_file_path: Optional[str] + _token: Optional[str] _http_client: AsyncClient _artifacts: ArtifactsAsyncClient @@ -2475,7 +2466,7 @@ def __init__(self, http_client: AsyncClient): raise Exception("The base url of the HTTP client must be set.") self._http_client = http_client - self._token_pair = None + self._token = None self._artifacts = ArtifactsAsyncClient(self) self._catalogs = CatalogsAsyncClient(self) @@ -2491,7 +2482,7 @@ def __init__(self, http_client: AsyncClient): @property def is_authenticated(self) -> bool: """Gets a value which indicates if the user is authenticated.""" - return self._token_pair is not None + return self._token is not None @property def artifacts(self) -> ArtifactsAsyncClient: @@ -2540,32 +2531,20 @@ def writers(self) -> WritersAsyncClient: - async def sign_in(self, refresh_token: str): + def sign_in(self, access_token: str): """Signs in the user. Args: - token_pair: The refresh token. + access_token: The access token. """ - actual_refresh_token: str - - sha256 = hashlib.sha256() - sha256.update(refresh_token.encode()) - refresh_token_hash = sha256.hexdigest() - self._token_file_path = os.path.join(self._token_folder_path, refresh_token_hash + ".json") - - if Path(self._token_file_path).is_file(): - with open(self._token_file_path) as file: - actual_refresh_token = file.read() + authorization_header_value = f"Bearer {access_token}" - else: - Path(self._token_folder_path).mkdir(parents=True, exist_ok=True) + if self._authorization_header_key in self._http_client.headers: + del self._http_client.headers[self._authorization_header_key] - with open(self._token_file_path, "w") as file: - file.write(refresh_token) - actual_refresh_token = refresh_token - - await self._refresh_token(actual_refresh_token) + self._http_client.headers[self._authorization_header_key] = authorization_header_value + self._token = access_token def attach_configuration(self, configuration: Any) -> Any: """Attaches configuration data to subsequent API requests. @@ -2600,42 +2579,14 @@ async def _invoke(self, typeOfT: Optional[Type[T]], method: str, relative_url: s # process response if not response.is_success: - # try to refresh the access token - if response.status_code == codes.UNAUTHORIZED and self._token_pair is not None: - - www_authenticate_header = response.headers.get("WWW-Authenticate") - sign_out = True - - if www_authenticate_header is not None: + message = response.text + status_code = f"N00.{response.status_code}" - if "The token expired at" in www_authenticate_header: + if not message: + raise NexusException(status_code, f"The HTTP request failed with status code {response.status_code}.") - try: - await self._refresh_token(self._token_pair.refresh_token) - - new_request = self._build_request_message(method, relative_url, content, content_type_value, accept_header_value) - new_response = await self._http_client.send(new_request) - - await response.aclose() - response = new_response - sign_out = False - - except: - pass - - if sign_out: - self._sign_out() - - if not response.is_success: - - message = response.text - status_code = f"N00.{response.status_code}" - - if not message: - raise NexusException(status_code, f"The HTTP request failed with status code {response.status_code}.") - - else: - raise NexusException(status_code, f"The HTTP request failed with status code {response.status_code}. The response message is: {message}") + else: + raise NexusException(status_code, f"The HTTP request failed with status code {response.status_code}. The response message is: {message}") try: @@ -2671,43 +2622,6 @@ def _build_request_message(self, method: str, relative_url: str, content: Any, c return request_message - async def _refresh_token(self, refresh_token: str): - self._mutex.acquire() - - try: - # make sure the refresh token has not already been redeemed - if (self._token_pair is not None and refresh_token != self._token_pair.refresh_token): - return - - # see https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/blob/dev/src/Microsoft.IdentityModel.Tokens/Validators.cs#L390 - - refresh_request = RefreshTokenRequest(refresh_token) - token_pair = await self.users.refresh_token(refresh_request) - - if self._token_file_path is not None: - Path(self._token_folder_path).mkdir(parents=True, exist_ok=True) - - with open(self._token_file_path, "w") as file: - file.write(token_pair.refresh_token) - - authorizationHeaderValue = f"Bearer {token_pair.access_token}" - - if self._authorization_header_key in self._http_client.headers: - del self._http_client.headers[self._authorization_header_key] - - self._http_client.headers[self._authorization_header_key] = authorizationHeaderValue - self._token_pair = token_pair - - finally: - self._mutex.release() - - def _sign_out(self) -> None: - - if self._authorization_header_key in self._http_client.headers: - del self._http_client.headers[self._authorization_header_key] - - self._token_pair = None - # "disposable" methods async def __aenter__(self) -> NexusAsyncClient: return self @@ -2911,11 +2825,7 @@ class NexusClient: _configuration_header_key: str = "Nexus-Configuration" _authorization_header_key: str = "Authorization" - _token_folder_path: str = os.path.join(str(Path.home()), "", "tokens") - _mutex: Lock = Lock() - - _token_pair: Optional[TokenPair] - _token_file_path: Optional[str] + _token: Optional[str] _http_client: Client _artifacts: ArtifactsClient @@ -2951,7 +2861,7 @@ def __init__(self, http_client: Client): raise Exception("The base url of the HTTP client must be set.") self._http_client = http_client - self._token_pair = None + self._token = None self._artifacts = ArtifactsClient(self) self._catalogs = CatalogsClient(self) @@ -2967,7 +2877,7 @@ def __init__(self, http_client: Client): @property def is_authenticated(self) -> bool: """Gets a value which indicates if the user is authenticated.""" - return self._token_pair is not None + return self._token is not None @property def artifacts(self) -> ArtifactsClient: @@ -3016,32 +2926,20 @@ def writers(self) -> WritersClient: - def sign_in(self, refresh_token: str): + def sign_in(self, access_token: str): """Signs in the user. Args: - token_pair: The refresh token. + access_token: The access token. """ - actual_refresh_token: str - - sha256 = hashlib.sha256() - sha256.update(refresh_token.encode()) - refresh_token_hash = sha256.hexdigest() - self._token_file_path = os.path.join(self._token_folder_path, refresh_token_hash + ".json") - - if Path(self._token_file_path).is_file(): - with open(self._token_file_path) as file: - actual_refresh_token = file.read() + authorization_header_value = f"Bearer {access_token}" - else: - Path(self._token_folder_path).mkdir(parents=True, exist_ok=True) + if self._authorization_header_key in self._http_client.headers: + del self._http_client.headers[self._authorization_header_key] - with open(self._token_file_path, "w") as file: - file.write(refresh_token) - actual_refresh_token = refresh_token - - self._refresh_token(actual_refresh_token) + self._http_client.headers[self._authorization_header_key] = authorization_header_value + self._token = access_token def attach_configuration(self, configuration: Any) -> Any: """Attaches configuration data to subsequent API requests. @@ -3076,42 +2974,14 @@ def _invoke(self, typeOfT: Optional[Type[T]], method: str, relative_url: str, ac # process response if not response.is_success: - # try to refresh the access token - if response.status_code == codes.UNAUTHORIZED and self._token_pair is not None: - - www_authenticate_header = response.headers.get("WWW-Authenticate") - sign_out = True - - if www_authenticate_header is not None: + message = response.text + status_code = f"N00.{response.status_code}" - if "The token expired at" in www_authenticate_header: + if not message: + raise NexusException(status_code, f"The HTTP request failed with status code {response.status_code}.") - try: - self._refresh_token(self._token_pair.refresh_token) - - new_request = self._build_request_message(method, relative_url, content, content_type_value, accept_header_value) - new_response = self._http_client.send(new_request) - - response.close() - response = new_response - sign_out = False - - except: - pass - - if sign_out: - self._sign_out() - - if not response.is_success: - - message = response.text - status_code = f"N00.{response.status_code}" - - if not message: - raise NexusException(status_code, f"The HTTP request failed with status code {response.status_code}.") - - else: - raise NexusException(status_code, f"The HTTP request failed with status code {response.status_code}. The response message is: {message}") + else: + raise NexusException(status_code, f"The HTTP request failed with status code {response.status_code}. The response message is: {message}") try: @@ -3147,43 +3017,6 @@ def _build_request_message(self, method: str, relative_url: str, content: Any, c return request_message - def _refresh_token(self, refresh_token: str): - self._mutex.acquire() - - try: - # make sure the refresh token has not already been redeemed - if (self._token_pair is not None and refresh_token != self._token_pair.refresh_token): - return - - # see https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/blob/dev/src/Microsoft.IdentityModel.Tokens/Validators.cs#L390 - - refresh_request = RefreshTokenRequest(refresh_token) - token_pair = self.users.refresh_token(refresh_request) - - if self._token_file_path is not None: - Path(self._token_folder_path).mkdir(parents=True, exist_ok=True) - - with open(self._token_file_path, "w") as file: - file.write(token_pair.refresh_token) - - authorizationHeaderValue = f"Bearer {token_pair.access_token}" - - if self._authorization_header_key in self._http_client.headers: - del self._http_client.headers[self._authorization_header_key] - - self._http_client.headers[self._authorization_header_key] = authorizationHeaderValue - self._token_pair = token_pair - - finally: - self._mutex.release() - - def _sign_out(self) -> None: - - if self._authorization_header_key in self._http_client.headers: - del self._http_client.headers[self._authorization_header_key] - - self._token_pair = None - # "disposable" methods def __enter__(self) -> NexusClient: return self From 75627d593bd8d5dafdc7a31f752a6ef10d5025ca Mon Sep 17 00:00:00 2001 From: Apollo3zehn Date: Thu, 7 Mar 2024 19:05:00 +0100 Subject: [PATCH 08/19] Fix samples and tests --- samples/C#/sample_export.ipynb | 4 +- samples/C#/sample_load.dib | 4 +- samples/python/sample_export.ipynb | 10 +- samples/python/sample_export_async.ipynb | 8 +- samples/python/sample_load.ipynb | 8 +- samples/python/sample_load_async.ipynb | 8 +- .../Nexus.ClientGenerator.csproj | 2 +- src/Nexus.ClientGenerator/Program.cs | 4 +- .../dotnet-client-tests/ClientTests.cs | 125 +----------------- .../python-client-tests/async-client-tests.py | 75 ++--------- .../python-client-tests/sync-client-tests.py | 71 ++-------- 11 files changed, 41 insertions(+), 278 deletions(-) diff --git a/samples/C#/sample_export.ipynb b/samples/C#/sample_export.ipynb index ed1e45e4..a1038886 100644 --- a/samples/C#/sample_export.ipynb +++ b/samples/C#/sample_export.ipynb @@ -53,11 +53,11 @@ "// - You get this token in the Nexus GUI's user menu. \n", "// - To avoid the token being invalidated by Nexus, do not use it in parallel.\n", "// - Best practice: Create one token per script or one token per \"thread\".\n", - "var refreshToken = \"\";\n", + "var accessToken = \"\";\n", "var uri = new Uri(\"http://localhost:5000\");\n", "var client = new NexusClient(uri);\n", "\n", - "await client.SignInAsync(refreshToken);" + "await client.SignInAsync(accessToken);" ] }, { diff --git a/samples/C#/sample_load.dib b/samples/C#/sample_load.dib index 5c4432f5..aebfe81d 100644 --- a/samples/C#/sample_load.dib +++ b/samples/C#/sample_load.dib @@ -20,11 +20,11 @@ using Nexus.Api; // - You get this token in the Nexus GUI's user menu. // - To avoid the token being invalidated by Nexus, do not use it in parallel. // - Best practice: Create one token per script or one token per "thread". -var refreshToken = ""; +var accessToken = ""; var uri = new Uri("http://localhost:5000"); var client = new NexusClient(uri); -await client.SignInAsync(refreshToken); +await client.SignInAsync(accessToken); #!markdown diff --git a/samples/python/sample_export.ipynb b/samples/python/sample_export.ipynb index 22db1da8..86c5f9a5 100644 --- a/samples/python/sample_export.ipynb +++ b/samples/python/sample_export.ipynb @@ -34,14 +34,12 @@ "source": [ "from nexus_api import NexusClient\n", "\n", - "# - You get this token in the Nexus GUI's user menu. \n", - "# - To avoid the token being invalidated by Nexus, do not use it in parallel.\n", - "# - Best practice: Create one token per script or one token per \"thread\".\n", - "refresh_token = \"\"\n", + "# - You get this token in the user settings menu of Nexus.\n", + "access_token = \"\"\n", "base_url = \"http://localhost:5000\"\n", "client = NexusClient.create(base_url)\n", "\n", - "client.sign_in(refresh_token)" + "client.sign_in(access_token)" ] }, { @@ -117,7 +115,7 @@ "mimetype": "text/x-csharp", "name": "python", "pygments_lexer": "csharp", - "version": "3.10.7" + "version": "3.11.6" }, "vscode": { "interpreter": { diff --git a/samples/python/sample_export_async.ipynb b/samples/python/sample_export_async.ipynb index a5a5d467..6a667a25 100644 --- a/samples/python/sample_export_async.ipynb +++ b/samples/python/sample_export_async.ipynb @@ -34,14 +34,12 @@ "source": [ "from nexus_api import NexusAsyncClient\n", "\n", - "# - You get this token in the Nexus GUI's user menu. \n", - "# - To avoid the token being invalidated by Nexus, do not use it in parallel.\n", - "# - Best practice: Create one token per script or one token per \"thread\".\n", - "refresh_token = \"\"\n", + "# - You get this token in the user settings menu of Nexus.\n", + "access_token = \"\"\n", "base_url = \"http://localhost:5000\"\n", "client = NexusAsyncClient.create(base_url)\n", "\n", - "await client.sign_in(refresh_token)" + "await client.sign_in(access_token)" ] }, { diff --git a/samples/python/sample_load.ipynb b/samples/python/sample_load.ipynb index a164b571..0bd0c0c4 100644 --- a/samples/python/sample_load.ipynb +++ b/samples/python/sample_load.ipynb @@ -30,14 +30,12 @@ "source": [ "from nexus_api import NexusClient\n", "\n", - "# - You get this token in the Nexus GUI's user menu. \n", - "# - To avoid the token being invalidated by Nexus, do not use it in parallel.\n", - "# - Best practice: Create one token per script or one token per \"thread\".\n", - "refresh_token = \"\"\n", + "# - You get this token in the user settings menu of Nexus.\n", + "access_token = \"\"\n", "base_url = \"http://localhost:5000\"\n", "client = NexusClient.create(base_url)\n", "\n", - "client.sign_in(refresh_token)" + "client.sign_in(access_token)" ] }, { diff --git a/samples/python/sample_load_async.ipynb b/samples/python/sample_load_async.ipynb index a0c03a23..0cf952d3 100644 --- a/samples/python/sample_load_async.ipynb +++ b/samples/python/sample_load_async.ipynb @@ -30,14 +30,12 @@ "source": [ "from nexus_api import NexusAsyncClient\n", "\n", - "# - You get this token in the Nexus GUI's user menu. \n", - "# - To avoid the token being invalidated by Nexus, do not use it in parallel.\n", - "# - Best practice: Create one token per script or one token per \"thread\".\n", - "refresh_token = \"\"\n", + "# - You get this token in the user settings menu of Nexus.\n", + "access_token = \"\"\n", "base_url = \"http://localhost:5000\"\n", "client = NexusAsyncClient.create(base_url)\n", "\n", - "await client.sign_in(refresh_token)" + "await client.sign_in(access_token)" ] }, { diff --git a/src/Nexus.ClientGenerator/Nexus.ClientGenerator.csproj b/src/Nexus.ClientGenerator/Nexus.ClientGenerator.csproj index 63d3a96b..0c25d460 100644 --- a/src/Nexus.ClientGenerator/Nexus.ClientGenerator.csproj +++ b/src/Nexus.ClientGenerator/Nexus.ClientGenerator.csproj @@ -11,7 +11,7 @@ - + \ No newline at end of file diff --git a/src/Nexus.ClientGenerator/Program.cs b/src/Nexus.ClientGenerator/Program.cs index 94e7acfa..058c55ec 100644 --- a/src/Nexus.ClientGenerator/Program.cs +++ b/src/Nexus.ClientGenerator/Program.cs @@ -60,9 +60,9 @@ public static async Task Main(string[] args) ConfigurationHeaderKey: "Nexus-Configuration", ExceptionType: "NexusException", ExceptionCodePrefix: "N", - GetOperationName: (path, type, operation) => operation.OperationId.Split(new[] { '_' }, 2)[1], + GetOperationName: (path, type, operation) => operation.OperationId.Split(['_'], 2)[1], Special_WebAssemblySupport: true, - Special_RefreshTokenSupport: true, + Special_AccessTokenSupport: true, Special_NexusFeatures: true); // generate C# client diff --git a/tests/clients/dotnet-client-tests/ClientTests.cs b/tests/clients/dotnet-client-tests/ClientTests.cs index 0619fd7d..49b68113 100644 --- a/tests/clients/dotnet-client-tests/ClientTests.cs +++ b/tests/clients/dotnet-client-tests/ClientTests.cs @@ -11,127 +11,6 @@ public class ClientTests { public const string NexusConfigurationHeaderKey = "Nexus-Configuration"; - [Fact] - public async Task CanAuthenticateAndRefreshAsync() - { - // Arrange - var messageHandlerMock = new Mock(); - var refreshToken = Guid.NewGuid().ToString(); - - // -> refresh token 1 - var refreshTokenTryCount = 0; - - var tokenPair1 = new TokenPair( - AccessToken: "111", - RefreshToken: "222" - ); - - messageHandlerMock - .Protected() - .Setup>( - "SendAsync", - ItExpr.Is(x => x.RequestUri!.ToString().EndsWith("tokens/refresh") && refreshTokenTryCount == 0), - ItExpr.IsAny()) - .Callback((requestMessage, cancellationToken) => - { - var refreshTokenRequest = JsonSerializer.Deserialize(requestMessage.Content!.ReadAsStream(cancellationToken)); - Assert.Equal(refreshToken, refreshTokenRequest!.RefreshToken); - refreshTokenTryCount++; - }) - .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent(JsonSerializer.Serialize(tokenPair1), Encoding.UTF8, "application/json") - }); - - // -> get catalogs (1st try) - var catalogTryCount = 0; - - var catalogsResponseMessage1 = new HttpResponseMessage() - { - StatusCode = HttpStatusCode.Unauthorized - }; - - catalogsResponseMessage1.Headers.Add("WWW-Authenticate", "Bearer The token expired at ..."); - - messageHandlerMock - .Protected() - .Setup>( - "SendAsync", - ItExpr.Is(x => x.RequestUri!.ToString().Contains("catalogs") && catalogTryCount == 0), - ItExpr.IsAny()) - .Callback((requestMessage, cancellationToken) => - { - var actual = requestMessage.Headers.Authorization!; - Assert.Equal($"Bearer {tokenPair1.AccessToken}", $"{actual.Scheme} {actual.Parameter}"); - catalogsResponseMessage1.RequestMessage = requestMessage; - catalogTryCount++; - }) - .ReturnsAsync(catalogsResponseMessage1); - - // -> refresh token 2 - var tokenPair2 = new TokenPair( - AccessToken: "333", - RefreshToken: "444" - ); - - messageHandlerMock - .Protected() - .Setup>( - "SendAsync", - ItExpr.Is(x => x.RequestUri!.ToString().EndsWith("tokens/refresh") && refreshTokenTryCount == 1), - ItExpr.IsAny()) - .Callback((requestMessage, cancellationToken) => - { - var refreshTokenRequest = JsonSerializer.Deserialize(requestMessage.Content!.ReadAsStream(cancellationToken)); - Assert.Equal(tokenPair1.RefreshToken, refreshTokenRequest!.RefreshToken); - }) - .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent(JsonSerializer.Serialize(tokenPair2), Encoding.UTF8, "application/json") - }); - - // -> get catalogs (2nd try) - var catalogId = "my-catalog-id"; - var expectedCatalog = new ResourceCatalog(Id: catalogId, default, default); - - var catalogsResponseMessage2 = new HttpResponseMessage() - { - StatusCode = HttpStatusCode.OK, - Content = new StringContent(JsonSerializer.Serialize(expectedCatalog), Encoding.UTF8, "application/json"), - }; - - messageHandlerMock - .Protected() - .Setup>( - "SendAsync", - ItExpr.Is(x => x.RequestUri!.ToString().Contains("catalogs") && catalogTryCount == 1), - ItExpr.IsAny()) - .Callback((requestMessage, cancellationToken) => - { - var actual = requestMessage.Headers.Authorization!; - Assert.Equal($"Bearer {tokenPair2.AccessToken}", $"{actual.Scheme} {actual.Parameter}"); - }) - .ReturnsAsync(catalogsResponseMessage2); - - // -> http client - var httpClient = new HttpClient(messageHandlerMock.Object) - { - BaseAddress = new Uri("http://localhost") - }; - - // -> API client - var client = new NexusClient(httpClient); - - // Act - await client.SignInAsync(refreshToken); - var actualCatalog = await client.Catalogs.GetAsync(catalogId); - - // Assert - Assert.Equal( - JsonSerializer.Serialize(expectedCatalog), - JsonSerializer.Serialize(actualCatalog)); - } - [Fact] public async Task CanAddConfigurationAsync() { @@ -192,12 +71,12 @@ public async Task CanAddConfigurationAsync() var encodedJson = Convert.ToBase64String(JsonSerializer.SerializeToUtf8Bytes(configuration)); Assert.Collection(actualHeaders, - headers => Assert.Null(headers), + Assert.Null, headers => { Assert.NotNull(headers); Assert.Collection(headers, header => Assert.Equal(encodedJson, header)); }, - headers => Assert.Null(headers)); + Assert.Null); } } } diff --git a/tests/clients/python-client-tests/async-client-tests.py b/tests/clients/python-client-tests/async-client-tests.py index 1c56a92a..816f1d3b 100644 --- a/tests/clients/python-client-tests/async-client-tests.py +++ b/tests/clients/python-client-tests/async-client-tests.py @@ -1,79 +1,24 @@ import base64 import json -import uuid import pytest from httpx import AsyncClient, MockTransport, Request, Response, codes -from nexus_api import NexusAsyncClient, ResourceCatalog +from nexus_api import NexusAsyncClient nexus_configuration_header_key = "Nexus-Configuration" -refresh_token = str(uuid.uuid1()) -refresh_token_try_count: int = 0 -catalog_try_count: int = 0 +try_count: int = 0 -def _handler1(request: Request): - global refresh_token - global catalog_try_count - global refresh_token_try_count +def _handler(request: Request): + global try_count if "catalogs" in request.url.path: - catalog_try_count += 1 - actual = request.headers["Authorization"] + try_count += 1 - if catalog_try_count == 1: - assert f"Bearer 111" == actual - return Response(codes.UNAUTHORIZED, headers={"WWW-Authenticate" : "Bearer The token expired at ..."}) - - else: - catalog_json_string = '{"Id":"my-catalog-id","Properties":null,"Resources":null}' - assert f"Bearer 333" == actual - return Response(codes.OK, content=catalog_json_string) - - elif "tokens/refresh" in request.url.path: - refresh_token_try_count += 1 - requestContent = request.content.decode("utf-8") - - if refresh_token_try_count == 1: - assert f'{{"refreshToken": "{refresh_token}"}}' == requestContent - return Response(codes.OK, content='{ "accessToken": "111", "refreshToken": "222" }') - - else: - assert '{"refreshToken": "222"}' == requestContent - return Response(codes.OK, content='{ "accessToken": "333", "refreshToken": "444" }') - - else: - raise Exception("Unsupported path.") - -@pytest.mark.asyncio -async def can_authenticate_and_refresh_test(): - - # arrange - catalog_id = "my-catalog-id" - expected_catalog = ResourceCatalog(catalog_id, None, None) - http_client = AsyncClient(base_url="http://localhost", transport=MockTransport(_handler1)) - - async with NexusAsyncClient(http_client) as client: - - # act - await client.sign_in(refresh_token) - actual_catalog = await client.catalogs.get(catalog_id) - - # assert - assert expected_catalog == actual_catalog - -try_count2: int = 0 - -def _handler2(request: Request): - global try_count2 - - if "catalogs" in request.url.path: - try_count2 += 1 - - if (try_count2 == 1): + if (try_count == 1): assert not nexus_configuration_header_key in request.headers - elif (try_count2 == 2): + elif (try_count == 2): configuration = { "foo1": "bar1", @@ -85,7 +30,7 @@ def _handler2(request: Request): assert expected == actual - elif (try_count2 == 3): + elif (try_count == 3): assert not nexus_configuration_header_key in request.headers catalog_json_string = '{"Id":"my-catalog-id","Properties":null,"Resources":null}' @@ -112,7 +57,7 @@ async def can_add_configuration_test(): "foo2": "bar2" } - http_client = AsyncClient(base_url="http://localhost", transport=MockTransport(_handler2)) + http_client = AsyncClient(base_url="http://localhost", transport=MockTransport(_handler)) async with NexusAsyncClient(http_client) as client: @@ -124,4 +69,4 @@ async def can_add_configuration_test(): _ = await client.catalogs.get(catalog_id) - # assert (already asserted in _handler2) + # assert (already asserted in _handler) diff --git a/tests/clients/python-client-tests/sync-client-tests.py b/tests/clients/python-client-tests/sync-client-tests.py index ddd78543..312787ee 100644 --- a/tests/clients/python-client-tests/sync-client-tests.py +++ b/tests/clients/python-client-tests/sync-client-tests.py @@ -7,71 +7,18 @@ nexus_configuration_header_key = "Nexus-Configuration" -refresh_token = str(uuid.uuid1()) -refresh_token_try_count: int = 0 -catalog_try_count: int = 0 +try_count: int = 0 -def _handler1(request: Request): - global refresh_token - global catalog_try_count - global refresh_token_try_count +def _handler(request: Request): + global try_count if "catalogs" in request.url.path: - catalog_try_count += 1 - actual = request.headers["Authorization"] + try_count += 1 - if catalog_try_count == 1: - assert f"Bearer 111" == actual - return Response(codes.UNAUTHORIZED, headers={"WWW-Authenticate" : "Bearer The token expired at ..."}) - - else: - catalog_json_string = '{"Id":"my-catalog-id","Properties":null,"Resources":null}' - assert f"Bearer 333" == actual - return Response(codes.OK, content=catalog_json_string) - - elif "tokens/refresh" in request.url.path: - refresh_token_try_count += 1 - requestContent = request.content.decode("utf-8") - - if refresh_token_try_count == 1: - assert f'{{"refreshToken": "{refresh_token}"}}' == requestContent - return Response(codes.OK, content='{ "accessToken": "111", "refreshToken": "222" }') - - else: - assert '{"refreshToken": "222"}' == requestContent - return Response(codes.OK, content='{ "accessToken": "333", "refreshToken": "444" }') - - else: - raise Exception("Unsupported path.") - -def can_authenticate_and_refresh_test(): - - # arrange - catalog_id = "my-catalog-id" - expected_catalog = ResourceCatalog(catalog_id, None, None) - http_client = Client(base_url="http://localhost", transport=MockTransport(_handler1)) - - with NexusClient(http_client) as client: - - # act - client.sign_in(refresh_token) - actual_catalog = client.catalogs.get(catalog_id) - - # assert - assert expected_catalog == actual_catalog - -try_count2: int = 0 - -def _handler2(request: Request): - global try_count2 - - if "catalogs" in request.url.path: - try_count2 += 1 - - if (try_count2 == 1): + if (try_count == 1): assert not nexus_configuration_header_key in request.headers - elif (try_count2 == 2): + elif (try_count == 2): configuration = { "foo1": "bar1", @@ -83,7 +30,7 @@ def _handler2(request: Request): assert expected == actual - elif (try_count2 == 3): + elif (try_count == 3): assert not nexus_configuration_header_key in request.headers catalog_json_string = '{"Id":"my-catalog-id","Properties":null,"Resources":null}' @@ -109,7 +56,7 @@ def can_add_configuration_test(): "foo2": "bar2" } - http_client = Client(base_url="http://localhost", transport=MockTransport(_handler2)) + http_client = Client(base_url="http://localhost", transport=MockTransport(_handler)) with NexusClient(http_client) as client: @@ -121,4 +68,4 @@ def can_add_configuration_test(): _ = client.catalogs.get(catalog_id) - # assert (already asserted in _handler2) + # assert (already asserted in _handler) From 16f6203884ae7f62631d702a4504935319faa101 Mon Sep 17 00:00:00 2001 From: Apollo3zehn Date: Thu, 7 Mar 2024 19:05:00 +0100 Subject: [PATCH 09/19] Improve code style --- .../Nexus.Tests/Services/CacheServiceTests.cs | 365 +++++++------ .../Services/CatalogManagerTests.cs | 507 +++++++++--------- .../Services/DataControllerServiceTests.cs | 191 ++++--- .../Nexus.Tests/Services/DataServiceTests.cs | 304 ++++++----- .../Services/ExtensionHiveTests.cs | 129 +++-- .../Services/MemoryTrackerTests.cs | 91 ++-- .../Services/ProcessingServiceTests.cs | 179 +++---- 7 files changed, 879 insertions(+), 887 deletions(-) diff --git a/tests/Nexus.Tests/Services/CacheServiceTests.cs b/tests/Nexus.Tests/Services/CacheServiceTests.cs index 55a2d68b..548ab419 100644 --- a/tests/Nexus.Tests/Services/CacheServiceTests.cs +++ b/tests/Nexus.Tests/Services/CacheServiceTests.cs @@ -5,225 +5,224 @@ using System.Runtime.InteropServices; using Xunit; -namespace Services +namespace Services; + +public class CacheServiceTests { - public class CacheServiceTests - { - delegate bool GobbleReturns(CatalogItem catalogItem, DateTime begin, out Stream cacheEntry); + delegate bool GobbleReturns(CatalogItem catalogItem, DateTime begin, out Stream cacheEntry); - [Fact] - public async Task CanReadCache() + [Fact] + public async Task CanReadCache() + { + // Arrange + var expected = new double[] { - // Arrange - var expected = new double[] + 0, 0, 2.2, 3.3, 4.4, 0, 6.6, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 10, 20, 30, 40, 50, 60, 70 + }; + + var databaseService = Mock.Of(); + + Mock.Get(databaseService) + .Setup(databaseService => databaseService.TryReadCacheEntry( + It.IsAny(), + It.IsAny(), + out It.Ref.IsAny)) + .Returns(new GobbleReturns((CatalogItem catalogItem, DateTime begin, out Stream cacheEntry) => { - 0, 0, 2.2, 3.3, 4.4, 0, 6.6, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 10, 20, 30, 40, 50, 60, 70 - }; - - var databaseService = Mock.Of(); - - Mock.Get(databaseService) - .Setup(databaseService => databaseService.TryReadCacheEntry( - It.IsAny(), - It.IsAny(), - out It.Ref.IsAny)) - .Returns(new GobbleReturns((CatalogItem catalogItem, DateTime begin, out Stream cacheEntry) => + cacheEntry = new MemoryStream(); + var writer = new BinaryWriter(cacheEntry); + + if (begin == new DateTime(2020, 01, 01, 0, 0, 0, DateTimeKind.Utc)) { - cacheEntry = new MemoryStream(); - var writer = new BinaryWriter(cacheEntry); + 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)) + }; - if (begin == new DateTime(2020, 01, 01, 0, 0, 0, DateTimeKind.Utc)) + for (int i = 0; i < 8; i++) { - 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)) - }; + writer.Write(i * 1.1); + } - for (int i = 0; i < 8; i++) - { - writer.Write(i * 1.1); - } + CacheEntryWrapper.WriteCachedIntervals(cacheEntry, cachedIntervals); - CacheEntryWrapper.WriteCachedIntervals(cacheEntry, cachedIntervals); + return true; + } - return true; - } + else if (begin == new DateTime(2020, 01, 02, 0, 0, 0, DateTimeKind.Utc)) + { + return false; + } - else if (begin == new DateTime(2020, 01, 02, 0, 0, 0, DateTimeKind.Utc)) + else if (begin == new DateTime(2020, 01, 03, 0, 0, 0, DateTimeKind.Utc)) + { + var cachedIntervals = new[] { - return false; - } + new Interval(new DateTime(2020, 01, 03, 0, 0, 0, DateTimeKind.Utc), new DateTime(2020, 01, 04, 0, 0, 0, DateTimeKind.Utc)) + }; - else if (begin == new DateTime(2020, 01, 03, 0, 0, 0, DateTimeKind.Utc)) + for (int i = 0; i < 8; i++) { - var cachedIntervals = new[] - { - new Interval(new DateTime(2020, 01, 03, 0, 0, 0, DateTimeKind.Utc), new DateTime(2020, 01, 04, 0, 0, 0, DateTimeKind.Utc)) - }; + writer.Write(i * 10.0); + } - for (int i = 0; i < 8; i++) - { - writer.Write(i * 10.0); - } + CacheEntryWrapper.WriteCachedIntervals(cacheEntry, cachedIntervals); - CacheEntryWrapper.WriteCachedIntervals(cacheEntry, cachedIntervals); + return true; + } - return true; - } + else + { + throw new Exception("This should never happen."); + } + })); - else - { - throw new Exception("This should never happen."); - } - })); + var cacheService = new CacheService(databaseService); - var cacheService = new CacheService(databaseService); + var catalogItem = new CatalogItem( + default!, + default!, + new Representation(NexusDataType.FLOAT64, TimeSpan.FromHours(3)), + default!); - var catalogItem = new CatalogItem( - default!, - default!, - new Representation(NexusDataType.FLOAT64, TimeSpan.FromHours(3)), - default!); + var begin = new DateTime(2020, 01, 01, 0, 0, 0, DateTimeKind.Utc); + var actual = new double[24]; - var begin = new DateTime(2020, 01, 01, 0, 0, 0, DateTimeKind.Utc); - var actual = new double[24]; + // Act + var uncachedIntervals = await cacheService + .ReadAsync(catalogItem, begin, actual, CancellationToken.None); - // Act - var uncachedIntervals = await cacheService - .ReadAsync(catalogItem, begin, actual, CancellationToken.None); + // Assert + Assert.Equal(expected.Length, actual.Length); - // Assert - 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); - } + var expected1 = new Interval( + Begin: new DateTime(2020, 01, 01, 0, 0, 0, DateTimeKind.Utc), + End: new DateTime(2020, 01, 01, 6, 0, 0, DateTimeKind.Utc)); - var expected1 = new Interval( - Begin: new DateTime(2020, 01, 01, 0, 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)); + var expected3 = new Interval( + Begin: new DateTime(2020, 01, 01, 21, 0, 0, DateTimeKind.Utc), + End: new DateTime(2020, 01, 03, 0, 0, 0, DateTimeKind.Utc)); - var expected3 = new Interval( - Begin: new DateTime(2020, 01, 01, 21, 0, 0, DateTimeKind.Utc), - End: new DateTime(2020, 01, 03, 0, 0, 0, DateTimeKind.Utc)); + Assert.Collection(uncachedIntervals, + actual1 => Assert.Equal(expected1, actual1), + actual2 => Assert.Equal(expected2, actual2), + actual3 => Assert.Equal(expected3, actual3)); + } - Assert.Collection(uncachedIntervals, - actual1 => Assert.Equal(expected1, actual1), - actual2 => Assert.Equal(expected2, actual2), - actual3 => Assert.Equal(expected3, actual3)); - } + [Fact] + public async Task CanUpdateCache() + { + // Arrange + var expectedData1 = new double[] { 0, 0, 0, 0, 0, 5, 6, 7 }; + var expectedData2 = new double[] { 8, 0, 0, 0, 0, 0, 0, 0 }; - [Fact] - public async Task CanUpdateCache() + var expected = new DateTime[] { - // Arrange - var expectedData1 = new double[] { 0, 0, 0, 0, 0, 5, 6, 7 }; - var expectedData2 = new double[] { 8, 0, 0, 0, 0, 0, 0, 0 }; - - var expected = new DateTime[] - { - new DateTime(2020, 01, 01, 0, 0, 0, DateTimeKind.Utc), - new DateTime(2020, 01, 02, 0, 0, 0, DateTimeKind.Utc) - }; - - var databaseService = Mock.Of(); - var actualBegins = new List(); - var actualStreams = new List(); - - Mock.Get(databaseService) - .Setup(databaseService => databaseService.TryWriteCacheEntry( - It.IsAny(), - It.IsAny(), - out It.Ref.IsAny)) - .Returns(new GobbleReturns((CatalogItem catalogItem, DateTime begin, out Stream cacheEntry) => - { - var stream = new MemoryStream(); - - cacheEntry = stream; - actualStreams.Add(stream); - actualBegins.Add(begin); - - return true; - })); - - var cacheService = new CacheService(databaseService); - - var catalogItem = new CatalogItem( - default!, - default!, - new Representation(NexusDataType.FLOAT64, TimeSpan.FromHours(3)), - default!); - - var sourceBuffer = Enumerable.Range(0, 24) - .Select(value => (double)value).ToArray(); - - var uncachedIntervals = new List + new DateTime(2020, 01, 01, 0, 0, 0, DateTimeKind.Utc), + new DateTime(2020, 01, 02, 0, 0, 0, DateTimeKind.Utc) + }; + + var databaseService = Mock.Of(); + var actualBegins = new List(); + var actualStreams = new List(); + + Mock.Get(databaseService) + .Setup(databaseService => databaseService.TryWriteCacheEntry( + It.IsAny(), + It.IsAny(), + out It.Ref.IsAny)) + .Returns(new GobbleReturns((CatalogItem catalogItem, DateTime begin, out Stream cacheEntry) => { - new Interval(new DateTime(2020, 01, 01, 15, 0, 0, DateTimeKind.Utc), new DateTime(2020, 01, 02, 03, 0, 0, DateTimeKind.Utc)) - }; + var stream = new MemoryStream(); - var begin = new DateTime(2020, 01, 01, 0, 0, 0, DateTimeKind.Utc); + cacheEntry = stream; + actualStreams.Add(stream); + actualBegins.Add(begin); - // Act - await cacheService - .UpdateAsync(catalogItem, begin, sourceBuffer, uncachedIntervals, CancellationToken.None); + return true; + })); - // Assert - Assert.True(expected.SequenceEqual(actualBegins)); + var cacheService = new CacheService(databaseService); - var actualData1 = MemoryMarshal.Cast(actualStreams[0].GetBuffer().AsSpan())[..8].ToArray(); - Assert.True(expectedData1.SequenceEqual(actualData1)); + var catalogItem = new CatalogItem( + default!, + default!, + new Representation(NexusDataType.FLOAT64, TimeSpan.FromHours(3)), + default!); - var actualData2 = MemoryMarshal.Cast(actualStreams[1].GetBuffer().AsSpan())[..8].ToArray(); - Assert.True(expectedData2.SequenceEqual(actualData2)); - } + var sourceBuffer = Enumerable.Range(0, 24) + .Select(value => (double)value).ToArray(); - [Fact] - public async Task CanClearCache() + var uncachedIntervals = new List { - // Arrange - var databaseService = Mock.Of(); - var catalogId = "foo"; - var begin = new DateTime(2020, 01, 01, 23, 0, 0, DateTimeKind.Utc); - var end = new DateTime(2020, 01, 03, 01, 0, 0, DateTimeKind.Utc); - var cacheService = new CacheService(databaseService); - - // Act - await cacheService - .ClearAsync(catalogId, begin, end, new Progress(), CancellationToken.None); - - // Assert - Mock.Get(databaseService).Verify(databaseService - => databaseService.ClearCacheEntriesAsync( - catalogId, - new DateOnly(2020, 01, 01), - It.IsAny(), - It.Is>(arg => !arg("2020-01-01T00-00-00-0000000") && arg("2020-01-01T23-00-00-0000000"))), - Times.Once); - - Mock.Get(databaseService).Verify(databaseService - => databaseService.ClearCacheEntriesAsync( - catalogId, - new DateOnly(2020, 01, 02), - It.IsAny(), - It.Is>(arg => arg("2020-01-02T00-00-00-0000000") && arg("bar"))), - Times.Once); - - Mock.Get(databaseService).Verify(databaseService - => databaseService.ClearCacheEntriesAsync( - catalogId, - new DateOnly(2020, 01, 03), - It.IsAny(), - It.Is>(arg => arg("2020-01-03T00-00-00-0000000") && !arg("2020-01-03T01-00-00-0000000"))), - Times.Once); - } + new Interval(new DateTime(2020, 01, 01, 15, 0, 0, DateTimeKind.Utc), new DateTime(2020, 01, 02, 03, 0, 0, DateTimeKind.Utc)) + }; + + var begin = new DateTime(2020, 01, 01, 0, 0, 0, DateTimeKind.Utc); + + // Act + await cacheService + .UpdateAsync(catalogItem, begin, sourceBuffer, uncachedIntervals, CancellationToken.None); + + // Assert + Assert.True(expected.SequenceEqual(actualBegins)); + + var actualData1 = MemoryMarshal.Cast(actualStreams[0].GetBuffer().AsSpan())[..8].ToArray(); + Assert.True(expectedData1.SequenceEqual(actualData1)); + + var actualData2 = MemoryMarshal.Cast(actualStreams[1].GetBuffer().AsSpan())[..8].ToArray(); + Assert.True(expectedData2.SequenceEqual(actualData2)); + } + + [Fact] + public async Task CanClearCache() + { + // Arrange + var databaseService = Mock.Of(); + var catalogId = "foo"; + var begin = new DateTime(2020, 01, 01, 23, 0, 0, DateTimeKind.Utc); + var end = new DateTime(2020, 01, 03, 01, 0, 0, DateTimeKind.Utc); + var cacheService = new CacheService(databaseService); + + // Act + await cacheService + .ClearAsync(catalogId, begin, end, new Progress(), CancellationToken.None); + + // Assert + Mock.Get(databaseService).Verify(databaseService + => databaseService.ClearCacheEntriesAsync( + catalogId, + new DateOnly(2020, 01, 01), + It.IsAny(), + It.Is>(arg => !arg("2020-01-01T00-00-00-0000000") && arg("2020-01-01T23-00-00-0000000"))), + Times.Once); + + Mock.Get(databaseService).Verify(databaseService + => databaseService.ClearCacheEntriesAsync( + catalogId, + new DateOnly(2020, 01, 02), + It.IsAny(), + It.Is>(arg => arg("2020-01-02T00-00-00-0000000") && arg("bar"))), + Times.Once); + + Mock.Get(databaseService).Verify(databaseService + => databaseService.ClearCacheEntriesAsync( + catalogId, + new DateOnly(2020, 01, 03), + It.IsAny(), + It.Is>(arg => arg("2020-01-03T00-00-00-0000000") && !arg("2020-01-03T01-00-00-0000000"))), + Times.Once); } -} +} \ No newline at end of file diff --git a/tests/Nexus.Tests/Services/CatalogManagerTests.cs b/tests/Nexus.Tests/Services/CatalogManagerTests.cs index e857200e..c474bf3f 100644 --- a/tests/Nexus.Tests/Services/CatalogManagerTests.cs +++ b/tests/Nexus.Tests/Services/CatalogManagerTests.cs @@ -10,277 +10,276 @@ using Xunit; using static OpenIddict.Abstractions.OpenIddictConstants; -namespace Services +namespace Services; + +public class CatalogManagerTests { - public class CatalogManagerTests + delegate bool GobbleReturns(string catalogId, out string catalogMetadata); + + [Fact] + public async Task CanCreateCatalogHierarchy() { - delegate bool GobbleReturns(string catalogId, out string catalogMetadata); + // Test case: + // User A, admin, + // / => /A, /B/A + // /A/ => /A/B, /A/B/C (should be ignored), /A/C/A + // + // User B, no admin, + // / => /A (should be ignored), /B/B, /B/B2, /C/A + + /* dataControllerService */ + var dataControllerService = Mock.Of(); + + Mock.Get(dataControllerService) + .Setup(s => s.GetDataSourceControllerAsync(It.IsAny(), It.IsAny())) + .Returns((registration, cancellationToken) => + { + var dataSourceController = Mock.Of(); - [Fact] - public async Task CanCreateCatalogHierarchy() - { - // Test case: - // User A, admin, - // / => /A, /B/A - // /A/ => /A/B, /A/B/C (should be ignored), /A/C/A - // - // User B, no admin, - // / => /A (should be ignored), /B/B, /B/B2, /C/A - - /* dataControllerService */ - var dataControllerService = Mock.Of(); - - Mock.Get(dataControllerService) - .Setup(s => s.GetDataSourceControllerAsync(It.IsAny(), It.IsAny())) - .Returns((registration, cancellationToken) => - { - var dataSourceController = Mock.Of(); + Mock.Get(dataSourceController) + .Setup(s => s.GetCatalogRegistrationsAsync(It.IsAny(), It.IsAny())) + .Returns((path, cancellationToken) => + { + var type = registration.Type; - Mock.Get(dataSourceController) - .Setup(s => s.GetCatalogRegistrationsAsync(It.IsAny(), It.IsAny())) - .Returns((path, cancellationToken) => + return (type, path) switch { - var type = registration.Type; - - return (type, path) switch - { - ("A", "/") => Task.FromResult(new CatalogRegistration[] { new CatalogRegistration("/A", string.Empty), new CatalogRegistration("/B/A", string.Empty) }), - ("A", "/A/") => Task.FromResult(new CatalogRegistration[] { new CatalogRegistration("/A/B", string.Empty), new CatalogRegistration("/A/B/C", string.Empty), new CatalogRegistration("/A/C/A", string.Empty) }), - ("B", "/") => Task.FromResult(new CatalogRegistration[] { new CatalogRegistration("/A", string.Empty), new CatalogRegistration("/B/B", string.Empty), new CatalogRegistration("/B/B2", string.Empty) }), - ("C", "/") => Task.FromResult(new CatalogRegistration[] { new CatalogRegistration("/C/A", string.Empty) }), - ("Nexus.Sources." + nameof(Sample), "/") => Task.FromResult(Array.Empty()), - _ => throw new Exception("Unsupported combination.") - }; - }); - - return Task.FromResult(dataSourceController); - }); - - /* appState */ - var registrationA = new InternalDataSourceRegistration(Id: Guid.NewGuid(), Type: "A", new Uri("", UriKind.Relative), default); - var registrationB = new InternalDataSourceRegistration(Id: Guid.NewGuid(), Type: "B", new Uri("", UriKind.Relative), default); - var registrationC = new InternalDataSourceRegistration(Id: Guid.NewGuid(), Type: "C", new Uri("", UriKind.Relative), default); - - var appState = new AppState() + ("A", "/") => Task.FromResult(new CatalogRegistration[] { new CatalogRegistration("/A", string.Empty), new CatalogRegistration("/B/A", string.Empty) }), + ("A", "/A/") => Task.FromResult(new CatalogRegistration[] { new CatalogRegistration("/A/B", string.Empty), new CatalogRegistration("/A/B/C", string.Empty), new CatalogRegistration("/A/C/A", string.Empty) }), + ("B", "/") => Task.FromResult(new CatalogRegistration[] { new CatalogRegistration("/A", string.Empty), new CatalogRegistration("/B/B", string.Empty), new CatalogRegistration("/B/B2", string.Empty) }), + ("C", "/") => Task.FromResult(new CatalogRegistration[] { new CatalogRegistration("/C/A", string.Empty) }), + ("Nexus.Sources." + nameof(Sample), "/") => Task.FromResult(Array.Empty()), + _ => throw new Exception("Unsupported combination.") + }; + }); + + return Task.FromResult(dataSourceController); + }); + + /* appState */ + var registrationA = new InternalDataSourceRegistration(Id: Guid.NewGuid(), Type: "A", new Uri("", UriKind.Relative), default); + var registrationB = new InternalDataSourceRegistration(Id: Guid.NewGuid(), Type: "B", new Uri("", UriKind.Relative), default); + var registrationC = new InternalDataSourceRegistration(Id: Guid.NewGuid(), Type: "C", new Uri("", UriKind.Relative), default); + + var appState = new AppState() + { + Project = new NexusProject(default!, default!, new Dictionary() { - Project = new NexusProject(default!, default!, new Dictionary() + ["UserA"] = new UserConfiguration(new Dictionary() { - ["UserA"] = new UserConfiguration(new Dictionary() - { - [Guid.NewGuid()] = registrationA - }), - ["UserB"] = new UserConfiguration(new Dictionary() - { - [Guid.NewGuid()] = registrationB, - [Guid.NewGuid()] = registrationC - }) + [Guid.NewGuid()] = registrationA + }), + ["UserB"] = new UserConfiguration(new Dictionary() + { + [Guid.NewGuid()] = registrationB, + [Guid.NewGuid()] = registrationC }) - }; - - // databaseService - var databaseService = Mock.Of(); - - Mock.Get(databaseService) - .Setup(databaseService => databaseService.TryReadCatalogMetadata( - It.IsAny(), - out It.Ref.IsAny)) - .Returns(new GobbleReturns((string catalogId, out string catalogMetadataString) => - { - catalogMetadataString = "{}"; - return true; - })); - - /* serviceProvider / dbService */ - var dbService = Mock.Of(); - var scope = Mock.Of(); - var scopeFactory = Mock.Of(); - var serviceProvider = Mock.Of(); - - Mock.Get(scope) - .SetupGet(scope => scope.ServiceProvider) - .Returns(serviceProvider); - - Mock.Get(scopeFactory) - .Setup(scopeFactory => scopeFactory.CreateScope()) - .Returns(scope); - - Mock.Get(serviceProvider) - .Setup(serviceProvider => serviceProvider.GetService( - It.Is(value => value == typeof(IDBService)))) - .Returns(dbService); - - Mock.Get(serviceProvider) - .Setup(serviceProvider => serviceProvider.GetService( - It.Is(value => value == typeof(IServiceScopeFactory)))) - .Returns(scopeFactory); - - /* => user A */ - var usernameA = "UserA"; - - var userAClaims = new List - { - new NexusClaim(Guid.NewGuid(), Claims.Name, usernameA), - new NexusClaim(Guid.NewGuid(), Claims.Role, NexusRoles.ADMINISTRATOR) - }; + }) + }; + + // databaseService + var databaseService = Mock.Of(); - var userA = new NexusUser( - id: string.Empty, - name: usernameA) + Mock.Get(databaseService) + .Setup(databaseService => databaseService.TryReadCatalogMetadata( + It.IsAny(), + out It.Ref.IsAny)) + .Returns(new GobbleReturns((string catalogId, out string catalogMetadataString) => { - Claims = userAClaims - }; + catalogMetadataString = "{}"; + return true; + })); + + /* serviceProvider / dbService */ + var dbService = Mock.Of(); + var scope = Mock.Of(); + var scopeFactory = Mock.Of(); + var serviceProvider = Mock.Of(); + + Mock.Get(scope) + .SetupGet(scope => scope.ServiceProvider) + .Returns(serviceProvider); + + Mock.Get(scopeFactory) + .Setup(scopeFactory => scopeFactory.CreateScope()) + .Returns(scope); + + Mock.Get(serviceProvider) + .Setup(serviceProvider => serviceProvider.GetService( + It.Is(value => value == typeof(IDBService)))) + .Returns(dbService); + + Mock.Get(serviceProvider) + .Setup(serviceProvider => serviceProvider.GetService( + It.Is(value => value == typeof(IServiceScopeFactory)))) + .Returns(scopeFactory); + + /* => user A */ + var usernameA = "UserA"; + + var userAClaims = new List + { + new NexusClaim(Guid.NewGuid(), Claims.Name, usernameA), + new NexusClaim(Guid.NewGuid(), Claims.Role, NexusRoles.ADMINISTRATOR) + }; + + var userA = new NexusUser( + id: string.Empty, + name: usernameA) + { + Claims = userAClaims + }; - /* => user B */ - var usernameB = "UserB"; + /* => user B */ + var usernameB = "UserB"; - var userBClaims = new List + var userBClaims = new List + { + new NexusClaim(Guid.NewGuid(), Claims.Name, usernameB), + }; + + var userB = new NexusUser( + id: string.Empty, + name: usernameB) + { + Claims = userBClaims + }; + + Mock.Get(dbService) + .Setup(dbService => dbService.FindUserAsync(It.IsAny())) + .Returns(userId => { - new NexusClaim(Guid.NewGuid(), Claims.Name, usernameB), - }; + var result = userId switch + { + "UserA" => Task.FromResult(userA), + "UserB" => Task.FromResult(userB), + _ => Task.FromResult(default) + }; + + return result; + }); + + /* extensionHive */ + var extensionHive = Mock.Of(); + + /* catalogManager */ + var catalogManager = new CatalogManager( + appState, + dataControllerService, + databaseService, + serviceProvider, + extensionHive, + NullLogger.Instance); + + // act + var root = CatalogContainer.CreateRoot(catalogManager, default!); + var rootCatalogContainers = (await root.GetChildCatalogContainersAsync(CancellationToken.None)).ToArray(); + var ACatalogContainers = (await rootCatalogContainers[0].GetChildCatalogContainersAsync(CancellationToken.None)).ToArray(); + + // assert '/' + Assert.Equal(5, rootCatalogContainers.Length); + + Assert.Contains( + rootCatalogContainers, + container => container.Id == "/A" && container.DataSourceRegistration == registrationA && container.Owner!.Identity!.Name! == userA.Name); + + Assert.Contains( + rootCatalogContainers, + container => container.Id == "/B/A" && container.DataSourceRegistration == registrationA && container.Owner!.Identity!.Name! == userA.Name); + + Assert.Contains( + rootCatalogContainers, + container => container.Id == "/B/B" && container.DataSourceRegistration == registrationB && container.Owner!.Identity!.Name! == userB.Name); + + Assert.Contains( + rootCatalogContainers, + container => container.Id == "/B/B2" && container.DataSourceRegistration == registrationB && container.Owner!.Identity!.Name! == userB.Name); + + Assert.Contains( + rootCatalogContainers, + container => container.Id == "/C/A" && container.DataSourceRegistration == registrationC && container.Owner!.Identity!.Name! == userB.Name); + + // assert 'A' + Assert.Equal(2, ACatalogContainers.Length); + + Assert.Contains( + ACatalogContainers, + container => container.Id == "/A/B" && container.DataSourceRegistration == registrationA && container.Owner!.Identity!.Name! == userA.Name); + + Assert.Contains( + ACatalogContainers, + container => container.Id == "/A/C/A" && container.DataSourceRegistration == registrationA && container.Owner!.Identity!.Name! == userA.Name); + } + + [Fact] + public async Task CanLoadLazyCatalogInfos() + { + // Arrange - var userB = new NexusUser( - id: string.Empty, - name: usernameB) + /* expected catalogs */ + var expectedCatalog = new ResourceCatalogBuilder(id: "/A") + .AddResource(new ResourceBuilder(id: "A").AddRepresentation(new Representation(NexusDataType.INT16, TimeSpan.FromSeconds(1))).Build()) + .WithReadme("v2") + .Build(); + + /* expected time range response */ + var expectedTimeRange = new CatalogTimeRange(new DateTime(2020, 01, 01), new DateTime(2020, 01, 02)); + + /* data controller service */ + var dataControllerService = Mock.Of(); + + Mock.Get(dataControllerService) + .Setup(s => s.GetDataSourceControllerAsync(It.IsAny(), It.IsAny())) + .Returns((registration, cancellationToken) => { - Claims = userBClaims - }; - - Mock.Get(dbService) - .Setup(dbService => dbService.FindUserAsync(It.IsAny())) - .Returns(userId => - { - var result = userId switch - { - "UserA" => Task.FromResult(userA), - "UserB" => Task.FromResult(userB), - _ => Task.FromResult(default) - }; - - return result; - }); - - /* extensionHive */ - var extensionHive = Mock.Of(); - - /* catalogManager */ - var catalogManager = new CatalogManager( - appState, - dataControllerService, - databaseService, - serviceProvider, - extensionHive, - NullLogger.Instance); - - // act - var root = CatalogContainer.CreateRoot(catalogManager, default!); - var rootCatalogContainers = (await root.GetChildCatalogContainersAsync(CancellationToken.None)).ToArray(); - var ACatalogContainers = (await rootCatalogContainers[0].GetChildCatalogContainersAsync(CancellationToken.None)).ToArray(); - - // assert '/' - Assert.Equal(5, rootCatalogContainers.Length); - - Assert.Contains( - rootCatalogContainers, - container => container.Id == "/A" && container.DataSourceRegistration == registrationA && container.Owner!.Identity!.Name! == userA.Name); - - Assert.Contains( - rootCatalogContainers, - container => container.Id == "/B/A" && container.DataSourceRegistration == registrationA && container.Owner!.Identity!.Name! == userA.Name); - - Assert.Contains( - rootCatalogContainers, - container => container.Id == "/B/B" && container.DataSourceRegistration == registrationB && container.Owner!.Identity!.Name! == userB.Name); - - Assert.Contains( - rootCatalogContainers, - container => container.Id == "/B/B2" && container.DataSourceRegistration == registrationB && container.Owner!.Identity!.Name! == userB.Name); - - Assert.Contains( - rootCatalogContainers, - container => container.Id == "/C/A" && container.DataSourceRegistration == registrationC && container.Owner!.Identity!.Name! == userB.Name); - - // assert 'A' - Assert.Equal(2, ACatalogContainers.Length); - - Assert.Contains( - ACatalogContainers, - container => container.Id == "/A/B" && container.DataSourceRegistration == registrationA && container.Owner!.Identity!.Name! == userA.Name); - - Assert.Contains( - ACatalogContainers, - container => container.Id == "/A/C/A" && container.DataSourceRegistration == registrationA && container.Owner!.Identity!.Name! == userA.Name); - } - - [Fact] - public async Task CanLoadLazyCatalogInfos() - { - // Arrange + var dataSourceController = Mock.Of(); - /* expected catalogs */ - var expectedCatalog = new ResourceCatalogBuilder(id: "/A") - .AddResource(new ResourceBuilder(id: "A").AddRepresentation(new Representation(NexusDataType.INT16, TimeSpan.FromSeconds(1))).Build()) - .WithReadme("v2") - .Build(); + Mock.Get(dataSourceController) + .Setup(s => s.GetCatalogAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(expectedCatalog); - /* expected time range response */ - var expectedTimeRange = new CatalogTimeRange(new DateTime(2020, 01, 01), new DateTime(2020, 01, 02)); + Mock.Get(dataSourceController) + .Setup(s => s.GetTimeRangeAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(expectedTimeRange); - /* data controller service */ - var dataControllerService = Mock.Of(); + return Task.FromResult(dataSourceController); + }); - Mock.Get(dataControllerService) - .Setup(s => s.GetDataSourceControllerAsync(It.IsAny(), It.IsAny())) - .Returns((registration, cancellationToken) => - { - var dataSourceController = Mock.Of(); - - Mock.Get(dataSourceController) - .Setup(s => s.GetCatalogAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(expectedCatalog); - - Mock.Get(dataSourceController) - .Setup(s => s.GetTimeRangeAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(expectedTimeRange); - - return Task.FromResult(dataSourceController); - }); - - /* catalog metadata */ - var catalogMetadata = new CatalogMetadata( - default, - default, - Overrides: new ResourceCatalogBuilder(id: "/A") - .WithReadme("v2") - .Build()); - - /* data source registrations */ - var registration = new InternalDataSourceRegistration( - Id: Guid.NewGuid(), - Type: "A", - ResourceLocator: default, - Configuration: default!); - - /* catalog container */ - var catalogContainer = new CatalogContainer( - new CatalogRegistration("/A", string.Empty), - default!, - registration, - default!, - catalogMetadata, - default!, - default!, - dataControllerService); - - // Act - var lazyCatalogInfo = await catalogContainer.GetLazyCatalogInfoAsync(CancellationToken.None); - - // Assert - var actualJsonString = JsonSerializerHelper.SerializeIndented(lazyCatalogInfo.Catalog); - var expectedJsonString = JsonSerializerHelper.SerializeIndented(expectedCatalog); - - Assert.Equal(actualJsonString, expectedJsonString); - Assert.Equal(new DateTime(2020, 01, 01), lazyCatalogInfo.Begin); - Assert.Equal(new DateTime(2020, 01, 02), lazyCatalogInfo.End); - } + /* catalog metadata */ + var catalogMetadata = new CatalogMetadata( + default, + default, + Overrides: new ResourceCatalogBuilder(id: "/A") + .WithReadme("v2") + .Build()); + + /* data source registrations */ + var registration = new InternalDataSourceRegistration( + Id: Guid.NewGuid(), + Type: "A", + ResourceLocator: default, + Configuration: default!); + + /* catalog container */ + var catalogContainer = new CatalogContainer( + new CatalogRegistration("/A", string.Empty), + default!, + registration, + default!, + catalogMetadata, + default!, + default!, + dataControllerService); + + // Act + var lazyCatalogInfo = await catalogContainer.GetLazyCatalogInfoAsync(CancellationToken.None); + + // Assert + var actualJsonString = JsonSerializerHelper.SerializeIndented(lazyCatalogInfo.Catalog); + var expectedJsonString = JsonSerializerHelper.SerializeIndented(expectedCatalog); + + Assert.Equal(actualJsonString, expectedJsonString); + Assert.Equal(new DateTime(2020, 01, 01), lazyCatalogInfo.Begin); + Assert.Equal(new DateTime(2020, 01, 02), lazyCatalogInfo.End); } -} +} \ No newline at end of file diff --git a/tests/Nexus.Tests/Services/DataControllerServiceTests.cs b/tests/Nexus.Tests/Services/DataControllerServiceTests.cs index a85a5afa..cd483667 100644 --- a/tests/Nexus.Tests/Services/DataControllerServiceTests.cs +++ b/tests/Nexus.Tests/Services/DataControllerServiceTests.cs @@ -11,123 +11,122 @@ using System.Text.Json; using Xunit; -namespace Services +namespace Services; + +public class DataControllerServiceTests { - public class DataControllerServiceTests + [Fact] + public async Task CanCreateAndInitializeDataSourceController() { - [Fact] - public async Task CanCreateAndInitializeDataSourceController() - { - // Arrange - var extensionHive = Mock.Of(); + // Arrange + var extensionHive = Mock.Of(); - Mock.Get(extensionHive) - .Setup(extensionHive => extensionHive.GetInstance(It.IsAny())) - .Returns(new Sample()); + Mock.Get(extensionHive) + .Setup(extensionHive => extensionHive.GetInstance(It.IsAny())) + .Returns(new Sample()); - var registration = new InternalDataSourceRegistration( - Id: Guid.NewGuid(), - Type: default!, - new Uri("A", UriKind.Relative), - Configuration: default); + var registration = new InternalDataSourceRegistration( + Id: Guid.NewGuid(), + Type: default!, + new Uri("A", UriKind.Relative), + Configuration: default); - var expectedCatalog = Sample.LoadCatalog("/A/B/C"); + var expectedCatalog = Sample.LoadCatalog("/A/B/C"); - var catalogState = new CatalogState( - Root: default!, - Cache: new CatalogCache() - ); + var catalogState = new CatalogState( + Root: default!, + Cache: new CatalogCache() + ); - var appState = new AppState() - { - Project = new NexusProject(default, default!, default!), - CatalogState = catalogState - }; + var appState = new AppState() + { + Project = new NexusProject(default, default!, default!), + CatalogState = catalogState + }; - var requestConfiguration = new Dictionary - { - ["foo"] = "bar", - ["foo2"] = "baz", - }; + var requestConfiguration = new Dictionary + { + ["foo"] = "bar", + ["foo2"] = "baz", + }; - var encodedRequestConfiguration = Convert.ToBase64String(JsonSerializer.SerializeToUtf8Bytes(requestConfiguration)); + var encodedRequestConfiguration = Convert.ToBase64String(JsonSerializer.SerializeToUtf8Bytes(requestConfiguration)); - var httpContext = new DefaultHttpContext(); - httpContext.Request.Headers.Add(DataControllerService.NexusConfigurationHeaderKey, encodedRequestConfiguration); + var httpContext = new DefaultHttpContext(); + httpContext.Request.Headers.Add(DataControllerService.NexusConfigurationHeaderKey, encodedRequestConfiguration); - var httpContextAccessor = Mock.Of(); + var httpContextAccessor = Mock.Of(); - Mock.Get(httpContextAccessor) - .SetupGet(httpContextAccessor => httpContextAccessor.HttpContext) - .Returns(httpContext); + Mock.Get(httpContextAccessor) + .SetupGet(httpContextAccessor => httpContextAccessor.HttpContext) + .Returns(httpContext); - var loggerFactory = Mock.Of(); + var loggerFactory = Mock.Of(); - Mock.Get(loggerFactory) - .Setup(loggerFactory => loggerFactory.CreateLogger(It.IsAny())) - .Returns(NullLogger.Instance); + Mock.Get(loggerFactory) + .Setup(loggerFactory => loggerFactory.CreateLogger(It.IsAny())) + .Returns(NullLogger.Instance); - var dataControllerService = new DataControllerService( - appState, - httpContextAccessor, - extensionHive, - default!, - default!, - Options.Create(new DataOptions()), - default!, - loggerFactory); + var dataControllerService = new DataControllerService( + appState, + httpContextAccessor, + extensionHive, + default!, + default!, + Options.Create(new DataOptions()), + default!, + loggerFactory); - // Act - var actual = await dataControllerService.GetDataSourceControllerAsync(registration, CancellationToken.None); + // Act + var actual = await dataControllerService.GetDataSourceControllerAsync(registration, CancellationToken.None); - // Assert - var actualCatalog = await actual.GetCatalogAsync("/A/B/C", CancellationToken.None); + // Assert + var actualCatalog = await actual.GetCatalogAsync("/A/B/C", CancellationToken.None); - Assert.Equal(expectedCatalog.Id, actualCatalog.Id); + Assert.Equal(expectedCatalog.Id, actualCatalog.Id); - var expectedConfig = JsonSerializer.Serialize(requestConfiguration); - var actualConfig = JsonSerializer.Serialize(((DataSourceController)actual).RequestConfiguration); + var expectedConfig = JsonSerializer.Serialize(requestConfiguration); + var actualConfig = JsonSerializer.Serialize(((DataSourceController)actual).RequestConfiguration); - Assert.Equal(expectedConfig, actualConfig); - } + Assert.Equal(expectedConfig, actualConfig); + } - [Fact] - public async Task CanCreateAndInitializeDataWriterController() + [Fact] + public async Task CanCreateAndInitializeDataWriterController() + { + // Arrange + var appState = new AppState() { - // Arrange - var appState = new AppState() - { - Project = new NexusProject(default, default!, default!) - }; - - var extensionHive = Mock.Of(); - - Mock.Get(extensionHive) - .Setup(extensionHive => extensionHive.GetInstance(It.IsAny())) - .Returns(new Csv()); - - var loggerFactory = Mock.Of(); - var resourceLocator = new Uri("A", UriKind.Relative); - var exportParameters = new ExportParameters(default, default, default, "dummy", default!, default); - - // Act - var dataControllerService = new DataControllerService( - appState, - default!, - extensionHive, - default!, - default!, - Options.Create(new DataOptions()), - default!, - loggerFactory); - - var actual = await dataControllerService.GetDataWriterControllerAsync( - resourceLocator, - exportParameters, - CancellationToken.None); - - // Assert - /* nothing to assert */ - } + Project = new NexusProject(default, default!, default!) + }; + + var extensionHive = Mock.Of(); + + Mock.Get(extensionHive) + .Setup(extensionHive => extensionHive.GetInstance(It.IsAny())) + .Returns(new Csv()); + + var loggerFactory = Mock.Of(); + var resourceLocator = new Uri("A", UriKind.Relative); + var exportParameters = new ExportParameters(default, default, default, "dummy", default!, default); + + // Act + var dataControllerService = new DataControllerService( + appState, + default!, + extensionHive, + default!, + default!, + Options.Create(new DataOptions()), + default!, + loggerFactory); + + var actual = await dataControllerService.GetDataWriterControllerAsync( + resourceLocator, + exportParameters, + CancellationToken.None); + + // Assert + /* nothing to assert */ } } \ No newline at end of file diff --git a/tests/Nexus.Tests/Services/DataServiceTests.cs b/tests/Nexus.Tests/Services/DataServiceTests.cs index 2d2014b7..032e623c 100644 --- a/tests/Nexus.Tests/Services/DataServiceTests.cs +++ b/tests/Nexus.Tests/Services/DataServiceTests.cs @@ -1,5 +1,4 @@ using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; using Moq; using Nexus.Core; using Nexus.DataModel; @@ -8,173 +7,172 @@ using System.IO.Compression; using Xunit; -namespace Services +namespace Services; + +public class DataServiceTests { - public class DataServiceTests - { - delegate void GobbleReturns(string catalogId, string searchPattern, EnumerationOptions enumerationOptions, out Stream attachment); + delegate void GobbleReturns(string catalogId, string searchPattern, EnumerationOptions enumerationOptions, out Stream attachment); - [Fact] - public async Task CanExportAsync() - { - // create dirs - var root = Path.Combine(Path.GetTempPath(), $"Nexus.Tests.{Guid.NewGuid()}"); - Directory.CreateDirectory(root); - - // misc - var begin = new DateTime(2020, 01, 01, 0, 0, 0, DateTimeKind.Utc); - var end = new DateTime(2020, 01, 03, 0, 0, 0, DateTimeKind.Utc); - var samplePeriod = TimeSpan.FromSeconds(1); - var exportId = Guid.NewGuid(); - - var registration1 = new InternalDataSourceRegistration(Id: Guid.NewGuid(), Type: "A", new Uri("a", UriKind.Relative), default, default); - var registration2 = new InternalDataSourceRegistration(Id: Guid.NewGuid(), Type: "B", new Uri("a", UriKind.Relative), default, default); - - // DI services - var dataSourceController1 = Mock.Of(); - var dataSourceController2 = Mock.Of(); - - var dataWriterController = Mock.Of(); - Uri tmpUri = default!; - - Mock.Get(dataWriterController) - .Setup(s => s.WriteAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny())) - .Callback, CancellationToken>( - (begin, end, samplePeriod, filePeriod, catalogItemRequestPipeReaders, progress, cancellationToken) => - { - foreach (var catalogItemRequestPipeReaderGroup in catalogItemRequestPipeReaders.GroupBy(x => x.Request.Item.Catalog)) - { - var prefix = catalogItemRequestPipeReaderGroup.Key.Id.TrimStart('/').Replace('/', '_'); - var filePath = Path.Combine(tmpUri.LocalPath, $"{prefix}.dat"); - File.Create(filePath).Dispose(); - } - }); - - var dataControllerService = Mock.Of(); - - Mock.Get(dataControllerService) - .Setup(s => s.GetDataSourceControllerAsync(It.IsAny(), It.IsAny())) - .Returns((registration, cancellationToken) => + [Fact] + public async Task CanExportAsync() + { + // create dirs + var root = Path.Combine(Path.GetTempPath(), $"Nexus.Tests.{Guid.NewGuid()}"); + Directory.CreateDirectory(root); + + // misc + var begin = new DateTime(2020, 01, 01, 0, 0, 0, DateTimeKind.Utc); + var end = new DateTime(2020, 01, 03, 0, 0, 0, DateTimeKind.Utc); + var samplePeriod = TimeSpan.FromSeconds(1); + var exportId = Guid.NewGuid(); + + var registration1 = new InternalDataSourceRegistration(Id: Guid.NewGuid(), Type: "A", new Uri("a", UriKind.Relative), default, default); + var registration2 = new InternalDataSourceRegistration(Id: Guid.NewGuid(), Type: "B", new Uri("a", UriKind.Relative), default, default); + + // DI services + var dataSourceController1 = Mock.Of(); + var dataSourceController2 = Mock.Of(); + + var dataWriterController = Mock.Of(); + Uri tmpUri = default!; + + Mock.Get(dataWriterController) + .Setup(s => s.WriteAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny())) + .Callback, CancellationToken>( + (begin, end, samplePeriod, filePeriod, catalogItemRequestPipeReaders, progress, cancellationToken) => + { + foreach (var catalogItemRequestPipeReaderGroup in catalogItemRequestPipeReaders.GroupBy(x => x.Request.Item.Catalog)) { - if (registration.Type == registration1.Type) - return Task.FromResult(dataSourceController1); + var prefix = catalogItemRequestPipeReaderGroup.Key.Id.TrimStart('/').Replace('/', '_'); + var filePath = Path.Combine(tmpUri.LocalPath, $"{prefix}.dat"); + File.Create(filePath).Dispose(); + } + }); - else if (registration.Type == registration2.Type) - return Task.FromResult(dataSourceController2); + var dataControllerService = Mock.Of(); - else - throw new Exception("Invalid data source registration."); - }); + Mock.Get(dataControllerService) + .Setup(s => s.GetDataSourceControllerAsync(It.IsAny(), It.IsAny())) + .Returns((registration, cancellationToken) => + { + if (registration.Type == registration1.Type) + return Task.FromResult(dataSourceController1); - Mock.Get(dataControllerService) - .Setup(s => s.GetDataWriterControllerAsync(It.IsAny(), It.IsAny(), It.IsAny())) - .Returns((uri, exportParameters, cancellationToken) => - { - tmpUri = uri; - return Task.FromResult(dataWriterController); - }); - - var databaseService = Mock.Of(); - - Mock.Get(databaseService) - .Setup(databaseService => databaseService.TryReadFirstAttachment( - It.IsAny(), - It.IsAny(), - It.IsAny(), - out It.Ref.IsAny)) - .Callback(new GobbleReturns((string catalogId, string searchPattern, EnumerationOptions enumerationOptions, out Stream attachment) => - { - attachment = new MemoryStream(); - })) - .Returns(true); - - Mock.Get(databaseService) - .Setup(databaseService => databaseService.WriteArtifact(It.IsAny())) - .Returns((fileName) => File.OpenWrite(Path.Combine(root, fileName))); - - var logger = Mock.Of>(); - var logger2 = Mock.Of>(); - - var loggerFactory = Mock.Of(); - - Mock.Get(loggerFactory) - .Setup(loggerFactory => loggerFactory.CreateLogger(It.IsAny())) - .Returns(logger2); - - 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)); - - // catalog items - var representation1 = new Representation(dataType: NexusDataType.FLOAT32, samplePeriod: samplePeriod); - var resource1 = new Resource(id: "Resource1"); - var catalog1 = new ResourceCatalog(id: "/A/B/C"); - var catalogItem1 = new CatalogItem(catalog1, resource1, representation1, Parameters: default); - var catalogContainer1 = new CatalogContainer(new CatalogRegistration(catalog1.Id, string.Empty), default!, registration1, default!, default!, default!, default!, default!); - - var representation2 = new Representation(dataType: NexusDataType.FLOAT32, samplePeriod: samplePeriod); - var resource2 = new Resource(id: "Resource2"); - var catalog2 = new ResourceCatalog(id: "/F/G/H"); - var catalogItem2 = new CatalogItem(catalog2, resource2, representation2, Parameters: default); - var catalogContainer2 = new CatalogContainer(new CatalogRegistration(catalog2.Id, string.Empty), default!, registration2, default!, default!, default!, default!, default!); - - // export parameters - var exportParameters = new ExportParameters( - Begin: begin, - End: end, - FilePeriod: TimeSpan.FromSeconds(10), - Type: "A", - ResourcePaths: new[] { catalogItem1.ToPath(), catalogItem2.ToPath() }, - Configuration: default); - - // data service - var dataService = new DataService( - default!, - default!, - dataControllerService, - databaseService, - memoryTracker, - logger, - loggerFactory); - - // act - try + else if (registration.Type == registration2.Type) + return Task.FromResult(dataSourceController2); + + else + throw new Exception("Invalid data source registration."); + }); + + Mock.Get(dataControllerService) + .Setup(s => s.GetDataWriterControllerAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns((uri, exportParameters, cancellationToken) => { - var catalogItemRequests = new[] - { - new CatalogItemRequest(catalogItem1, default, catalogContainer1), - new CatalogItemRequest(catalogItem2, default, catalogContainer2) - }; + tmpUri = uri; + return Task.FromResult(dataWriterController); + }); + + var databaseService = Mock.Of(); + + Mock.Get(databaseService) + .Setup(databaseService => databaseService.TryReadFirstAttachment( + It.IsAny(), + It.IsAny(), + It.IsAny(), + out It.Ref.IsAny)) + .Callback(new GobbleReturns((string catalogId, string searchPattern, EnumerationOptions enumerationOptions, out Stream attachment) => + { + attachment = new MemoryStream(); + })) + .Returns(true); + + Mock.Get(databaseService) + .Setup(databaseService => databaseService.WriteArtifact(It.IsAny())) + .Returns((fileName) => File.OpenWrite(Path.Combine(root, fileName))); + + var logger = Mock.Of>(); + var logger2 = Mock.Of>(); + + var loggerFactory = Mock.Of(); + + Mock.Get(loggerFactory) + .Setup(loggerFactory => loggerFactory.CreateLogger(It.IsAny())) + .Returns(logger2); + + 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)); + + // catalog items + var representation1 = new Representation(dataType: NexusDataType.FLOAT32, samplePeriod: samplePeriod); + var resource1 = new Resource(id: "Resource1"); + var catalog1 = new ResourceCatalog(id: "/A/B/C"); + var catalogItem1 = new CatalogItem(catalog1, resource1, representation1, Parameters: default); + var catalogContainer1 = new CatalogContainer(new CatalogRegistration(catalog1.Id, string.Empty), default!, registration1, default!, default!, default!, default!, default!); + + var representation2 = new Representation(dataType: NexusDataType.FLOAT32, samplePeriod: samplePeriod); + var resource2 = new Resource(id: "Resource2"); + var catalog2 = new ResourceCatalog(id: "/F/G/H"); + var catalogItem2 = new CatalogItem(catalog2, resource2, representation2, Parameters: default); + var catalogContainer2 = new CatalogContainer(new CatalogRegistration(catalog2.Id, string.Empty), default!, registration2, default!, default!, default!, default!, default!); + + // export parameters + var exportParameters = new ExportParameters( + Begin: begin, + End: end, + FilePeriod: TimeSpan.FromSeconds(10), + Type: "A", + ResourcePaths: new[] { catalogItem1.ToPath(), catalogItem2.ToPath() }, + Configuration: default); + + // data service + var dataService = new DataService( + default!, + default!, + dataControllerService, + databaseService, + memoryTracker, + logger, + loggerFactory); + + // act + try + { + var catalogItemRequests = new[] + { + new CatalogItemRequest(catalogItem1, default, catalogContainer1), + new CatalogItemRequest(catalogItem2, default, catalogContainer2) + }; - var relativeDownloadUrl = await dataService - .ExportAsync(Guid.NewGuid(), catalogItemRequests, default!, exportParameters, CancellationToken.None); + var relativeDownloadUrl = await dataService + .ExportAsync(Guid.NewGuid(), catalogItemRequests, default!, exportParameters, CancellationToken.None); - // assert - var zipFile = Path.Combine(root, relativeDownloadUrl.Split('/').Last()); - var unzipFolder = Path.GetDirectoryName(zipFile)!; + // assert + var zipFile = Path.Combine(root, relativeDownloadUrl.Split('/').Last()); + var unzipFolder = Path.GetDirectoryName(zipFile)!; - ZipFile.ExtractToDirectory(zipFile, unzipFolder); + ZipFile.ExtractToDirectory(zipFile, unzipFolder); - Assert.True(File.Exists(Path.Combine(unzipFolder, "A_B_C.dat"))); - Assert.True(File.Exists(Path.Combine(unzipFolder, "A_B_C_LICENSE.md"))); + Assert.True(File.Exists(Path.Combine(unzipFolder, "A_B_C.dat"))); + Assert.True(File.Exists(Path.Combine(unzipFolder, "A_B_C_LICENSE.md"))); - Assert.True(File.Exists(Path.Combine(unzipFolder, "F_G_H.dat"))); - Assert.True(File.Exists(Path.Combine(unzipFolder, "F_G_H_LICENSE.md"))); + Assert.True(File.Exists(Path.Combine(unzipFolder, "F_G_H.dat"))); + Assert.True(File.Exists(Path.Combine(unzipFolder, "F_G_H_LICENSE.md"))); + } + finally + { + try + { + Directory.Delete(root, true); } - finally + catch { - try - { - Directory.Delete(root, true); - } - catch - { - // - } + // } } } -} +} \ No newline at end of file diff --git a/tests/Nexus.Tests/Services/ExtensionHiveTests.cs b/tests/Nexus.Tests/Services/ExtensionHiveTests.cs index 1bf3713c..96d4c81c 100644 --- a/tests/Nexus.Tests/Services/ExtensionHiveTests.cs +++ b/tests/Nexus.Tests/Services/ExtensionHiveTests.cs @@ -8,91 +8,90 @@ using System.Diagnostics; using Xunit; -namespace Services +namespace Services; + +public class ExtensionHiveTests { - public class ExtensionHiveTests + [Fact] + public async Task CanInstantiateExtensionsAsync() { - [Fact] - public async Task CanInstantiateExtensionsAsync() - { - // prepare extension - var extensionFolderPath = Path.Combine(Path.GetTempPath(), $"Nexus.Tests.{Guid.NewGuid()}"); - var configuration = "Debug"; - var csprojPath = "./../../../../tests/TestExtensionProject/TestExtensionProject.csproj"; + // prepare extension + var extensionFolderPath = Path.Combine(Path.GetTempPath(), $"Nexus.Tests.{Guid.NewGuid()}"); + var configuration = "Debug"; + var csprojPath = "./../../../../tests/TestExtensionProject/TestExtensionProject.csproj"; - var process = new Process + var process = new Process + { + StartInfo = new ProcessStartInfo { - StartInfo = new ProcessStartInfo - { - FileName = "dotnet", - Arguments = $"publish --output {Path.Combine(extensionFolderPath, "v1.0.0-unit.test")} --configuration {configuration} {csprojPath}" - } - }; + FileName = "dotnet", + Arguments = $"publish --output {Path.Combine(extensionFolderPath, "v1.0.0-unit.test")} --configuration {configuration} {csprojPath}" + } + }; - process.Start(); - process.WaitForExit(); + process.Start(); + process.WaitForExit(); - Assert.Equal(0, process.ExitCode); + Assert.Equal(0, process.ExitCode); - // prepare restore root - var restoreRoot = Path.Combine(Path.GetTempPath(), $"Nexus.Tests.{Guid.NewGuid()}"); + // prepare restore root + var restoreRoot = Path.Combine(Path.GetTempPath(), $"Nexus.Tests.{Guid.NewGuid()}"); - try + try + { + // load packages + var pathsOptions = new PathsOptions() { - // load packages - var pathsOptions = new PathsOptions() - { - Packages = restoreRoot - }; - - var loggerFactory = Mock.Of(); + Packages = restoreRoot + }; - Mock.Get(loggerFactory) - .Setup(loggerFactory => loggerFactory.CreateLogger(It.IsAny())) - .Returns(NullLogger.Instance); + var loggerFactory = Mock.Of(); - var hive = new ExtensionHive(Options.Create(pathsOptions), NullLogger.Instance, loggerFactory); + Mock.Get(loggerFactory) + .Setup(loggerFactory => loggerFactory.CreateLogger(It.IsAny())) + .Returns(NullLogger.Instance); - var version = "v1.0.0-unit.test"; + var hive = new ExtensionHive(Options.Create(pathsOptions), NullLogger.Instance, loggerFactory); - var packageReference = new InternalPackageReference( - Id: Guid.NewGuid(), - Provider: "local", - Configuration: new Dictionary - { - // required - ["path"] = extensionFolderPath, - ["version"] = version - } - ); + var version = "v1.0.0-unit.test"; - var packageReferences = new[] + var packageReference = new InternalPackageReference( + Id: Guid.NewGuid(), + Provider: "local", + Configuration: new Dictionary { - packageReference - }; + // required + ["path"] = extensionFolderPath, + ["version"] = version + } + ); - await hive.LoadPackagesAsync(packageReferences, new Progress(), CancellationToken.None); + var packageReferences = new[] + { + packageReference + }; - // instantiate - hive.GetInstance("TestExtensionProject.TestDataSource"); - hive.GetInstance("TestExtensionProject.TestDataWriter"); + await hive.LoadPackagesAsync(packageReferences, new Progress(), CancellationToken.None); - Assert.Throws(() => hive.GetInstance("TestExtensionProject.TestDataWriter")); - } - finally + // instantiate + hive.GetInstance("TestExtensionProject.TestDataSource"); + hive.GetInstance("TestExtensionProject.TestDataWriter"); + + Assert.Throws(() => hive.GetInstance("TestExtensionProject.TestDataWriter")); + } + finally + { + try { - try - { - Directory.Delete(restoreRoot, recursive: true); - } - catch { } + Directory.Delete(restoreRoot, recursive: true); + } + catch { } - try - { - Directory.Delete(extensionFolderPath, recursive: true); - } - catch { } + try + { + Directory.Delete(extensionFolderPath, recursive: true); } + catch { } } } -} +} \ No newline at end of file diff --git a/tests/Nexus.Tests/Services/MemoryTrackerTests.cs b/tests/Nexus.Tests/Services/MemoryTrackerTests.cs index d40a1f10..025f5b4d 100644 --- a/tests/Nexus.Tests/Services/MemoryTrackerTests.cs +++ b/tests/Nexus.Tests/Services/MemoryTrackerTests.cs @@ -4,68 +4,67 @@ using Nexus.Services; using Xunit; -namespace Services +namespace Services; + +public class MemoryTrackerTests { - public class MemoryTrackerTests + [Fact] + public async Task CanHandleMultipleRequests() { - [Fact] - 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) { - // 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 - Factor = 2 - }; + // TODO: remove this property and test with factor 8 + Factor = 2 + }; - var firstRegistration = default(AllocationRegistration); - var secondRegistration = default(AllocationRegistration); + var firstRegistration = default(AllocationRegistration); + var secondRegistration = default(AllocationRegistration); - // Act - var registrationsTask = Task.Run(async () => - { - firstRegistration = await memoryTracker.RegisterAllocationAsync(minimumByteCount: 100, maximumByteCount: 100, CancellationToken.None); + // Act + var registrationsTask = Task.Run(async () => + { + firstRegistration = await memoryTracker.RegisterAllocationAsync(minimumByteCount: 100, maximumByteCount: 100, CancellationToken.None); - var firstWaitingTask = memoryTracker.RegisterAllocationAsync(minimumByteCount: 70, maximumByteCount: 70, CancellationToken.None); - var secondWaitingTask = memoryTracker.RegisterAllocationAsync(minimumByteCount: 80, maximumByteCount: 80, CancellationToken.None); + var firstWaitingTask = memoryTracker.RegisterAllocationAsync(minimumByteCount: 70, maximumByteCount: 70, CancellationToken.None); + var secondWaitingTask = memoryTracker.RegisterAllocationAsync(minimumByteCount: 80, maximumByteCount: 80, CancellationToken.None); - Assert.True(firstWaitingTask.Status != TaskStatus.RanToCompletion); - Assert.True(secondWaitingTask.Status != TaskStatus.RanToCompletion); + Assert.True(firstWaitingTask.Status != TaskStatus.RanToCompletion); + Assert.True(secondWaitingTask.Status != TaskStatus.RanToCompletion); - // dispose first registration - weAreWaiting.Set(); + // dispose first registration + weAreWaiting.Set(); - await Task.WhenAny(firstWaitingTask, secondWaitingTask); + await Task.WhenAny(firstWaitingTask, secondWaitingTask); - if (firstWaitingTask.Status == TaskStatus.RanToCompletion) - secondRegistration = await firstWaitingTask; + if (firstWaitingTask.Status == TaskStatus.RanToCompletion) + secondRegistration = await firstWaitingTask; - else if (secondWaitingTask.Status == TaskStatus.RanToCompletion) - secondRegistration = await secondWaitingTask; + else if (secondWaitingTask.Status == TaskStatus.RanToCompletion) + secondRegistration = await secondWaitingTask; - Assert.True(secondRegistration is not null); + Assert.True(secondRegistration is not null); - // dispose second registration - weAreWaiting.Set(); + // dispose second registration + weAreWaiting.Set(); - await Task.WhenAll(firstWaitingTask, secondWaitingTask); - }); + await Task.WhenAll(firstWaitingTask, secondWaitingTask); + }); - var ConsumingTask = Task.Run(() => - { - weAreWaiting.WaitOne(); - firstRegistration!.Dispose(); + var ConsumingTask = Task.Run(() => + { + weAreWaiting.WaitOne(); + firstRegistration!.Dispose(); - weAreWaiting.WaitOne(); - secondRegistration!.Dispose(); - }); + weAreWaiting.WaitOne(); + secondRegistration!.Dispose(); + }); - await registrationsTask; + await registrationsTask; - // Assert - } + // Assert } -} +} \ No newline at end of file diff --git a/tests/Nexus.Tests/Services/ProcessingServiceTests.cs b/tests/Nexus.Tests/Services/ProcessingServiceTests.cs index cceda57a..8f3c3514 100644 --- a/tests/Nexus.Tests/Services/ProcessingServiceTests.cs +++ b/tests/Nexus.Tests/Services/ProcessingServiceTests.cs @@ -5,105 +5,104 @@ using System.Runtime.InteropServices; using Xunit; -namespace Services +namespace Services; + +public class ProcessingServiceTests { - public class ProcessingServiceTests - { - [InlineData("Min", 0.90, new int[] { 0, 1, 2, 3, -4, 5, 6, 7, 0, 2, 97, 13 }, new byte[] { 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1 }, -4)] - [InlineData("Min", 0.99, new int[] { 0, 1, 2, 3, -4, 5, 6, 7, 0, 2, 97, 13 }, new byte[] { 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1 }, double.NaN)] + [InlineData("Min", 0.90, new int[] { 0, 1, 2, 3, -4, 5, 6, 7, 0, 2, 97, 13 }, new byte[] { 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1 }, -4)] + [InlineData("Min", 0.99, new int[] { 0, 1, 2, 3, -4, 5, 6, 7, 0, 2, 97, 13 }, new byte[] { 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1 }, double.NaN)] - [InlineData("Max", 0.90, new int[] { 0, 1, 2, 3, -4, 5, 6, 7, 0, 2, 97, 13 }, new byte[] { 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1 }, 97)] - [InlineData("Max", 0.99, new int[] { 0, 1, 2, 3, -4, 5, 6, 7, 0, 2, 97, 13 }, new byte[] { 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1 }, double.NaN)] + [InlineData("Max", 0.90, new int[] { 0, 1, 2, 3, -4, 5, 6, 7, 0, 2, 97, 13 }, new byte[] { 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1 }, 97)] + [InlineData("Max", 0.99, new int[] { 0, 1, 2, 3, -4, 5, 6, 7, 0, 2, 97, 13 }, new byte[] { 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1 }, double.NaN)] - [InlineData("Mean", 0.90, new int[] { 0, 1, 2, 3, -4, 5, 6, 7, 0, 2, 97, 13 }, new byte[] { 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1 }, 12)] - [InlineData("Mean", 0.99, new int[] { 0, 1, 2, 3, -4, 5, 6, 7, 0, 2, 97, 13 }, new byte[] { 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1 }, double.NaN)] + [InlineData("Mean", 0.90, new int[] { 0, 1, 2, 3, -4, 5, 6, 7, 0, 2, 97, 13 }, new byte[] { 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1 }, 12)] + [InlineData("Mean", 0.99, new int[] { 0, 1, 2, 3, -4, 5, 6, 7, 0, 2, 97, 13 }, new byte[] { 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1 }, double.NaN)] - [InlineData("MeanPolarDeg", 0.90, new int[] { 0, 1, 2, 3, -4, 5, 6, 7, 0, 2, 97, 13 }, new byte[] { 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1 }, 9.25)] - [InlineData("MeanPolarDeg", 0.99, new int[] { 0, 1, 2, 3, -4, 5, 6, 7, 0, 2, 97, 13 }, new byte[] { 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1 }, double.NaN)] + [InlineData("MeanPolarDeg", 0.90, new int[] { 0, 1, 2, 3, -4, 5, 6, 7, 0, 2, 97, 13 }, new byte[] { 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1 }, 9.25)] + [InlineData("MeanPolarDeg", 0.99, new int[] { 0, 1, 2, 3, -4, 5, 6, 7, 0, 2, 97, 13 }, new byte[] { 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1 }, double.NaN)] - [InlineData("Sum", 0.90, new int[] { 0, 1, 2, 3, -4, 5, 6, 7, 0, 2, 97, 13 }, new byte[] { 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1 }, 132)] - [InlineData("Sum", 0.99, new int[] { 0, 1, 2, 3, -4, 5, 6, 7, 0, 2, 97, 13 }, new byte[] { 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1 }, double.NaN)] + [InlineData("Sum", 0.90, new int[] { 0, 1, 2, 3, -4, 5, 6, 7, 0, 2, 97, 13 }, new byte[] { 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1 }, 132)] + [InlineData("Sum", 0.99, new int[] { 0, 1, 2, 3, -4, 5, 6, 7, 0, 2, 97, 13 }, new byte[] { 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1 }, double.NaN)] - [InlineData("MinBitwise", 0.90, new int[] { 2, 2, 2, 3, 2, 3, 65, 2, 98, 14 }, new byte[] { 1, 1, 1, 1, 1, 1, 0, 1, 1, 1 }, 2)] - [InlineData("MinBitwise", 0.99, new int[] { 2, 2, 2, 3, 2, 3, 65, 2, 98, 14 }, new byte[] { 1, 1, 1, 1, 1, 1, 0, 1, 1, 1 }, double.NaN)] + [InlineData("MinBitwise", 0.90, new int[] { 2, 2, 2, 3, 2, 3, 65, 2, 98, 14 }, new byte[] { 1, 1, 1, 1, 1, 1, 0, 1, 1, 1 }, 2)] + [InlineData("MinBitwise", 0.99, new int[] { 2, 2, 2, 3, 2, 3, 65, 2, 98, 14 }, new byte[] { 1, 1, 1, 1, 1, 1, 0, 1, 1, 1 }, double.NaN)] - [InlineData("MaxBitwise", 0.90, new int[] { 2, 2, 2, 3, 2, 3, 65, 2, 98, 14 }, new byte[] { 1, 1, 1, 1, 1, 1, 0, 1, 1, 1 }, 111)] - [InlineData("MaxBitwise", 0.99, new int[] { 2, 2, 2, 3, 2, 3, 65, 2, 98, 14 }, new byte[] { 1, 1, 1, 1, 1, 1, 0, 1, 1, 1 }, double.NaN)] + [InlineData("MaxBitwise", 0.90, new int[] { 2, 2, 2, 3, 2, 3, 65, 2, 98, 14 }, new byte[] { 1, 1, 1, 1, 1, 1, 0, 1, 1, 1 }, 111)] + [InlineData("MaxBitwise", 0.99, new int[] { 2, 2, 2, 3, 2, 3, 65, 2, 98, 14 }, new byte[] { 1, 1, 1, 1, 1, 1, 0, 1, 1, 1 }, double.NaN)] + + [Theory] + public void CanAggregateSingle(string kindString, double nanThreshold, int[] data, byte[] status, double expected) + { + // Arrange + var kind = Enum.Parse(kindString); + var options = Options.Create(new DataOptions() { AggregationNaNThreshold = nanThreshold }); + var processingService = new ProcessingService(options); + var blockSize = data.Length; + var actual = new double[1]; + var byteData = MemoryMarshal.AsBytes(data).ToArray(); + + // Act + processingService.Aggregate(NexusDataType.INT32, kind, byteData, status, targetBuffer: actual, blockSize); + + // Assert + Assert.Equal(expected, actual[0], precision: 2); + } - [Theory] - public void CanAggregateSingle(string kindString, double nanThreshold, int[] data, byte[] status, double expected) + [Fact] + public void CanAggregateMultiple() + { + // Arrange + var data = new int[] { - // Arrange - var kind = Enum.Parse(kindString); - var options = Options.Create(new DataOptions() { AggregationNaNThreshold = nanThreshold }); - var processingService = new ProcessingService(options); - var blockSize = data.Length; - var actual = new double[1]; - var byteData = MemoryMarshal.AsBytes(data).ToArray(); - - // Act - processingService.Aggregate(NexusDataType.INT32, kind, byteData, status, targetBuffer: actual, blockSize); - - // Assert - Assert.Equal(expected, actual[0], precision: 2); - } - - [Fact] - public void CanAggregateMultiple() + 0, 1, 2, 3, -4, 5, 6, 7, 0, 2, 97, 13, + 0, 1, 2, 3, -4, 5, 6, 7, 3, 2, 87, 12 + }; + + var status = new byte[] { - // Arrange - var data = new int[] - { - 0, 1, 2, 3, -4, 5, 6, 7, 0, 2, 97, 13, - 0, 1, 2, 3, -4, 5, 6, 7, 3, 2, 87, 12 - }; - - var status = new byte[] - { - 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, - 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 - }; - - var expected = new double[] { 132, 123 }; - var options = Options.Create(new DataOptions() { AggregationNaNThreshold = 0.9 }); - var processingService = new ProcessingService(options); - var blockSize = data.Length / 2; - var actual = new double[expected.Length]; - var byteData = MemoryMarshal.AsBytes(data).ToArray(); - - // Act - processingService.Aggregate(NexusDataType.INT32, RepresentationKind.Sum, byteData, status, targetBuffer: actual, blockSize); - - // Assert - Assert.True(expected.SequenceEqual(actual)); - } - - [Fact] - public void CanResample() + 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, + 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 + }; + + var expected = new double[] { 132, 123 }; + var options = Options.Create(new DataOptions() { AggregationNaNThreshold = 0.9 }); + var processingService = new ProcessingService(options); + var blockSize = data.Length / 2; + var actual = new double[expected.Length]; + var byteData = MemoryMarshal.AsBytes(data).ToArray(); + + // Act + processingService.Aggregate(NexusDataType.INT32, RepresentationKind.Sum, byteData, status, targetBuffer: actual, blockSize); + + // Assert + Assert.True(expected.SequenceEqual(actual)); + } + + [Fact] + public void CanResample() + { + // Arrange + var data = new float[] + { + 0, 1, 2, 3 + }; + + var status = new byte[] { - // Arrange - var data = new float[] - { - 0, 1, 2, 3 - }; - - var status = new byte[] - { - 1, 1, 0, 1 - }; - - var expected = new double[] { 0, 0, 1, 1, 1, 1, double.NaN, double.NaN, double.NaN, double.NaN, 3, 3 }; - var options = Options.Create(new DataOptions()); - var processingService = new ProcessingService(options); - var blockSize = 4; - var actual = new double[expected.Length]; - var byteData = MemoryMarshal.AsBytes(data).ToArray(); - - // Act - processingService.Resample(NexusDataType.FLOAT32, byteData, status, targetBuffer: actual, blockSize, offset: 2); - - // Assert - Assert.True(expected.SequenceEqual(actual)); - } + 1, 1, 0, 1 + }; + + var expected = new double[] { 0, 0, 1, 1, 1, 1, double.NaN, double.NaN, double.NaN, double.NaN, 3, 3 }; + var options = Options.Create(new DataOptions()); + var processingService = new ProcessingService(options); + var blockSize = 4; + var actual = new double[expected.Length]; + var byteData = MemoryMarshal.AsBytes(data).ToArray(); + + // Act + processingService.Resample(NexusDataType.FLOAT32, byteData, status, targetBuffer: actual, blockSize, offset: 2); + + // Assert + Assert.True(expected.SequenceEqual(actual)); } -} +} \ No newline at end of file From 42cc18b1bd4f2114f9900f38960d59eab377887e Mon Sep 17 00:00:00 2001 From: Apollo3zehn Date: Thu, 7 Mar 2024 19:05:00 +0100 Subject: [PATCH 10/19] Finish UI and backend changes, only actual authentication is missing --- notes/auth.md | 5 + openapi.json | 29 +-- samples/C#/sample_export.ipynb | 41 +--- samples/C#/sample_load.dib | 8 +- samples/matlab/sample_export.m | 8 +- samples/matlab/sample_load.m | 8 +- samples/python/sample_export.ipynb | 2 +- samples/python/sample_export_async.ipynb | 2 +- samples/python/sample_load.ipynb | 2 +- samples/python/sample_load_async.ipynb | 2 +- .../Components/UserSettingsView.razor | 183 ++++++++++++++---- src/Nexus.UI/Core/NexusDemoClient.cs | 55 ++---- src/Nexus/API/SourcesController.cs | 2 +- src/Nexus/API/UsersController.cs | 14 +- src/Nexus/Core/Models_Public.cs | 11 -- src/Nexus/Core/NexusAuthExtensions.cs | 10 +- src/Nexus/Services/DatabaseService.cs | 4 +- src/clients/dotnet-client/NexusClient.g.cs | 24 +-- src/clients/matlab-client/NexusClient.m | 135 +------------ .../python-client/nexus_api/_nexus_api.py | 29 +-- .../python-client-tests/async-client-tests.py | 7 - .../python-client-tests/sync-client-tests.py | 10 +- 22 files changed, 238 insertions(+), 353 deletions(-) diff --git a/notes/auth.md b/notes/auth.md index 59815a85..8f3035d4 100644 --- a/notes/auth.md +++ b/notes/auth.md @@ -1,3 +1,8 @@ +# Note +The text below does not apply anymore to Nexus because we have switched from refresh tokens + access tokens to personal access tokens that expire only optionally and are not cryptographically signed but checked against the database instead. The negible problem of higher database load is acceptible to get the benefit of not having to manage refresh tokens which are prone to being revoked as soon as the user uses it in more than a single place. + +The new personal access tokens approach allows fine-grained access control to catalogs and makes many parts of the code much simpler. + # Authentication and Authorization Nexus exposes resources (data, metadata and more) via HTTP API. Most of these resources do not have specific owners - they are owned by the system itself. Most of these resources need to be protected which makes an `authorization` mechanism necessary. diff --git a/openapi.json b/openapi.json index e9d40c0b..b6a73501 100644 --- a/openapi.json +++ b/openapi.json @@ -1325,12 +1325,12 @@ } ], "requestBody": { - "x-name": "request", - "description": "The create token request.", + "x-name": "token", + "description": "The personal access token to create.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateTokenRequest" + "$ref": "#/components/schemas/PersonalAccessToken" } } }, @@ -2271,29 +2271,6 @@ } } }, - "CreateTokenRequest": { - "type": "object", - "description": "A revoke token request.", - "additionalProperties": false, - "properties": { - "description": { - "type": "string", - "description": "The token description." - }, - "expires": { - "type": "string", - "description": "The date/time when the token expires.", - "format": "date-time" - }, - "claims": { - "type": "array", - "description": "The claims that will be part of the token.", - "items": { - "$ref": "#/components/schemas/TokenClaim" - } - } - } - }, "NexusClaim": { "type": "object", "description": "Represents a claim.", diff --git a/samples/C#/sample_export.ipynb b/samples/C#/sample_export.ipynb index a1038886..bd855658 100644 --- a/samples/C#/sample_export.ipynb +++ b/samples/C#/sample_export.ipynb @@ -2,26 +2,16 @@ "cells": [ { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": { "dotnet_interactive": { "language": "csharp" }, "vscode": { - "languageId": "dotnet-interactive.csharp" + "languageId": "polyglot-notebook" } }, - "outputs": [ - { - "data": { - "text/html": [ - "
Restore sources
  • https://www.myget.org/F/apollo3zehn-dev/api/v3/index.json
Installed Packages
  • Nexus.Api, 1.0.0-beta.11.258
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "// https://github.com/dotnet/interactive/issues/698\n", "#i \"nuget: https://www.myget.org/F/apollo3zehn-dev/api/v3/index.json\"\n", @@ -37,22 +27,20 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": { "dotnet_interactive": { "language": "csharp" }, "vscode": { - "languageId": "dotnet-interactive.csharp" + "languageId": "polyglot-notebook" } }, "outputs": [], "source": [ "using Nexus.Api;\n", "\n", - "// - You get this token in the Nexus GUI's user menu. \n", - "// - To avoid the token being invalidated by Nexus, do not use it in parallel.\n", - "// - Best practice: Create one token per script or one token per \"thread\".\n", + "// You get this token in the user settings menu of Nexus. \n", "var accessToken = \"\";\n", "var uri = new Uri(\"http://localhost:5000\");\n", "var client = new NexusClient(uri);\n", @@ -69,27 +57,16 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": { "dotnet_interactive": { "language": "csharp" }, "vscode": { - "languageId": "dotnet-interactive.csharp" + "languageId": "polyglot-notebook" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "export: 0 %\n", - "export: 100 %\n", - "download: 100 %\n", - "extract: 100 %\n" - ] - } - ], + "outputs": [], "source": [ "var begin = new DateTime(2020, 02, 01, 0, 0, 0, DateTimeKind.Utc);\n", "var end = new DateTime(2020, 02, 02, 0, 0, 0, DateTimeKind.Utc);\n", diff --git a/samples/C#/sample_load.dib b/samples/C#/sample_load.dib index aebfe81d..91b1d1b4 100644 --- a/samples/C#/sample_load.dib +++ b/samples/C#/sample_load.dib @@ -1,3 +1,7 @@ +#!meta + +{"kernelInfo":{"defaultKernelName":"csharp","items":[{"aliases":[],"name":"csharp"}]}} + #!markdown ### TODO @@ -17,9 +21,7 @@ Create client and authenticate using Nexus.Api; -// - You get this token in the Nexus GUI's user menu. -// - To avoid the token being invalidated by Nexus, do not use it in parallel. -// - Best practice: Create one token per script or one token per "thread". +// You get this token in the user settings menu of Nexus. var accessToken = ""; var uri = new Uri("http://localhost:5000"); var client = new NexusClient(uri); diff --git a/samples/matlab/sample_export.m b/samples/matlab/sample_export.m index 80b8feed..cf618b47 100644 --- a/samples/matlab/sample_export.m +++ b/samples/matlab/sample_export.m @@ -6,14 +6,12 @@ addpath(connectorFolderPath) %% Create client and authenticate -% - You get this token in the Nexus GUI's user menu. -% - To avoid the token being invalidated by Nexus, do not use it in parallel. -% - Best practice: Create one token per script or one token per "thread". -refreshToken = ''; +% You get this token in the user settings menu of Nexus. +accessToken = ''; baseUrl = 'http://localhost:5000'; client = NexusClient(baseUrl); -client.signIn(refreshToken) +client.signIn(accessToken) %% Export data from sample catalog /SAMPLE/LOCAL dateTimeBegin = datetime(2020, 01, 01, 0, 0, 0, 'TimeZone', 'UTC'); diff --git a/samples/matlab/sample_load.m b/samples/matlab/sample_load.m index 216b7244..d7cfa92d 100644 --- a/samples/matlab/sample_load.m +++ b/samples/matlab/sample_load.m @@ -6,14 +6,12 @@ addpath(connectorFolderPath) %% Create client and authenticate -% - You get this token in the Nexus GUI's user menu. -% - To avoid the token being invalidated by Nexus, do not use it in parallel. -% - Best practice: Create one token per script or one token per "thread". -refreshToken = ''; +% You get this token in the user settings menu of Nexus. +accessToken = ''; baseUrl = 'http://localhost:5000'; client = NexusClient(baseUrl); -client.signIn(refreshToken) +client.signIn(accessToken) %% Load data from sample catalog /SAMPLE/LOCAL dateTimeBegin = datetime(2020, 01, 01, 0, 0, 0, 'TimeZone', 'UTC'); diff --git a/samples/python/sample_export.ipynb b/samples/python/sample_export.ipynb index 86c5f9a5..b6eaac7c 100644 --- a/samples/python/sample_export.ipynb +++ b/samples/python/sample_export.ipynb @@ -34,7 +34,7 @@ "source": [ "from nexus_api import NexusClient\n", "\n", - "# - You get this token in the user settings menu of Nexus.\n", + "# You get this token in the user settings menu of Nexus.\n", "access_token = \"\"\n", "base_url = \"http://localhost:5000\"\n", "client = NexusClient.create(base_url)\n", diff --git a/samples/python/sample_export_async.ipynb b/samples/python/sample_export_async.ipynb index 6a667a25..4b6ec141 100644 --- a/samples/python/sample_export_async.ipynb +++ b/samples/python/sample_export_async.ipynb @@ -34,7 +34,7 @@ "source": [ "from nexus_api import NexusAsyncClient\n", "\n", - "# - You get this token in the user settings menu of Nexus.\n", + "# You get this token in the user settings menu of Nexus.\n", "access_token = \"\"\n", "base_url = \"http://localhost:5000\"\n", "client = NexusAsyncClient.create(base_url)\n", diff --git a/samples/python/sample_load.ipynb b/samples/python/sample_load.ipynb index 0bd0c0c4..3f0ece12 100644 --- a/samples/python/sample_load.ipynb +++ b/samples/python/sample_load.ipynb @@ -30,7 +30,7 @@ "source": [ "from nexus_api import NexusClient\n", "\n", - "# - You get this token in the user settings menu of Nexus.\n", + "# You get this token in the user settings menu of Nexus.\n", "access_token = \"\"\n", "base_url = \"http://localhost:5000\"\n", "client = NexusClient.create(base_url)\n", diff --git a/samples/python/sample_load_async.ipynb b/samples/python/sample_load_async.ipynb index 0cf952d3..2f4a822a 100644 --- a/samples/python/sample_load_async.ipynb +++ b/samples/python/sample_load_async.ipynb @@ -30,7 +30,7 @@ "source": [ "from nexus_api import NexusAsyncClient\n", "\n", - "# - You get this token in the user settings menu of Nexus.\n", + "# You get this token in the user settings menu of Nexus.\n", "access_token = \"\"\n", "base_url = \"http://localhost:5000\"\n", "client = NexusAsyncClient.create(base_url)\n", diff --git a/src/Nexus.UI/Components/UserSettingsView.razor b/src/Nexus.UI/Components/UserSettingsView.razor index 0a19d78b..6bba395a 100644 --- a/src/Nexus.UI/Components/UserSettingsView.razor +++ b/src/Nexus.UI/Components/UserSettingsView.razor @@ -15,11 +15,13 @@ + Settings + - +
New Token
- @if (_newRefreshToken is not null) + @if (_newAccessToken is not null) {
This is your newly generated token. Make a copy and store it safely as you will not be able to see it again.
- @_newRefreshToken + @_newAccessToken @@ -90,21 +92,91 @@ } else { -
- - - - Create - +
+ +
+ +
+ + + +
+ + + + @if (_newAccessTokenExpiration is null) + { + (expires never) + } + +
+ +
+ +
+ + @for (var i = 0; i < _newAccessTokenClaims.Count; i++) + { + var local = i; + +
+ + + + +
+ +
+
+ } + + + The catalog(s) you want to access (regex pattern) + + +
+ +
+ +
+ + Add + +
+ +
+ + Create Token + +
+
} } @@ -113,13 +185,13 @@
Available Tokens
- @if (_refreshTokenMap is null) + @if (_accessTokenMap is null) {
- Acquiring refresh tokens ... + Acquiring access tokens ...
} @@ -127,12 +199,12 @@ {
- @foreach (var (id, refreshToken) in _refreshTokenMap) + @foreach (var (id, accessToken) in _accessTokenMap) {
-
@refreshToken.Description
-
@(refreshToken.Expires == DateTime.MaxValue ? "expires never" : "expires on " + refreshToken.Expires.Date.ToString("yyyy-MM-dd"))
+
@accessToken.Description
+
@(accessToken.Expires == DateTime.MaxValue ? "expires never" : "expires on " + accessToken.Expires.Date.ToString("yyyy-MM-dd"))
+ +
+ @code { + private class ClaimData + { + public ClaimData(string? catalogPattern, bool writeAccess) + { + CatalogPattern = catalogPattern; + WriteAccess = writeAccess; + } + + public string? CatalogPattern { get; set; } + + public bool WriteAccess { get; set; } + } + private static JsonSerializerOptions _options = new JsonSerializerOptions() { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, @@ -196,9 +284,11 @@ private bool _isRefreshingDatabase; private CancellationTokenSource? _cts; private string? _jsonString; - private string? _newRefreshToken; - private string? _newRefreshTokenDescription; - private IReadOnlyDictionary? _refreshTokenMap; + private string? _newAccessToken; + private DateTime? _newAccessTokenExpiration; + private string? _newAccessTokenDescription; + private List _newAccessTokenClaims = [new ClaimData(default, default)]; + private IReadOnlyDictionary? _accessTokenMap; private bool _isRefreshingToken; @@ -220,7 +310,7 @@ try { - _refreshTokenMap = (await Client.Users.GetMeAsync()).RefreshTokens; + _accessTokenMap = (await Client.Users.GetMeAsync()).PersonalAccessTokens; } catch (Exception ex) { @@ -231,17 +321,17 @@ private void OpenUserSettingsModal() { _isUserSettingsDialogOpen = true; - _newRefreshToken = default; + _newAccessToken = default; } private void CopyToClipboard() { - JSRuntime.InvokeVoid("nexus.util.copyToClipboard", _newRefreshToken); + JSRuntime.InvokeVoid("nexus.util.copyToClipboard", _newAccessToken); } - private async Task GenerateTokenAsync() + private async Task CreateTokenAsync() { - if (_newRefreshTokenDescription is null) + if (_newAccessTokenDescription is null) return; _isRefreshingToken = true; @@ -249,9 +339,30 @@ try { - _newRefreshToken = await Client.Users.GenerateRefreshTokenAsync(_newRefreshTokenDescription); - _newRefreshTokenDescription = default; - _refreshTokenMap = (await Client.Users.GetMeAsync()).RefreshTokens; + var claims = _newAccessTokenClaims + .Where( + claimData => claimData is not null && + claimData.CatalogPattern is not null) + .Select(claimData => new TokenClaim( + claimData.WriteAccess ? "CanWriteCatalog" : "CanReadCatalog", + claimData.CatalogPattern!)) + .ToList(); + + var token = new PersonalAccessToken( + _newAccessTokenDescription, + _newAccessTokenExpiration.HasValue + ? _newAccessTokenExpiration.Value + : DateTime.MaxValue, + claims + ); + + _newAccessToken = await Client.Users.CreateTokenAsync(token); + + _newAccessTokenDescription = default; + _newAccessTokenExpiration = default; + _newAccessTokenClaims.Clear(); + _newAccessTokenClaims.Add(new ClaimData(default, default)); + _accessTokenMap = (await Client.Users.GetMeAsync()).PersonalAccessTokens; } catch (Exception ex) { @@ -266,8 +377,8 @@ { try { - await Client.Users.DeleteRefreshTokenAsync(id, CancellationToken.None); - _refreshTokenMap = (await Client.Users.GetMeAsync()).RefreshTokens; + await Client.Users.DeleteTokenAsync(id, CancellationToken.None); + _accessTokenMap = (await Client.Users.GetMeAsync()).PersonalAccessTokens; } catch (Exception ex) { diff --git a/src/Nexus.UI/Core/NexusDemoClient.cs b/src/Nexus.UI/Core/NexusDemoClient.cs index cb73b96c..8f2f9dfd 100644 --- a/src/Nexus.UI/Core/NexusDemoClient.cs +++ b/src/Nexus.UI/Core/NexusDemoClient.cs @@ -35,12 +35,7 @@ public void ClearConfiguration() throw new NotImplementedException(); } - public void SignIn(string refreshToken) - { - throw new NotImplementedException(); - } - - public Task SignInAsync(string refreshToken, CancellationToken cancellationToken) + public void SignIn(string accessToken) { throw new NotImplementedException(); } @@ -376,6 +371,16 @@ public Task CreateClaimAsync(string userId, NexusClaim claim, Cancellation throw new NotImplementedException(); } + public string CreateToken(PersonalAccessToken token, string? userId = null) + { + throw new NotImplementedException(); + } + + public Task CreateTokenAsync(PersonalAccessToken token, string? userId = null, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + public string CreateUser(NexusUser user) { throw new NotImplementedException(); @@ -396,32 +401,32 @@ public Task DeleteClaimAsync(Guid claimId, CancellationToke throw new NotImplementedException(); } - public HttpResponseMessage DeleteRefreshToken(Guid tokenId) + public HttpResponseMessage DeleteToken(Guid tokenId) { throw new NotImplementedException(); } - public Task DeleteRefreshTokenAsync(Guid tokenId, CancellationToken cancellationToken = default) + public Task DeleteTokenAsync(Guid tokenId, CancellationToken cancellationToken = default) { throw new NotImplementedException(); } - public HttpResponseMessage DeleteUser(string userId) + public HttpResponseMessage DeleteTokenByValue(string value) { throw new NotImplementedException(); } - public Task DeleteUserAsync(string userId, CancellationToken cancellationToken = default) + public Task DeleteTokenByValueAsync(string value, CancellationToken cancellationToken = default) { throw new NotImplementedException(); } - public string GenerateRefreshToken(string description, string? userId = null) + public HttpResponseMessage DeleteUser(string userId) { throw new NotImplementedException(); } - public Task GenerateRefreshTokenAsync(string description, string? userId, CancellationToken cancellationToken = default) + public Task DeleteUserAsync(string userId, CancellationToken cancellationToken = default) { throw new NotImplementedException(); } @@ -461,18 +466,18 @@ public Task GetMeAsync(CancellationToken cancellationToken = default UserId: "test@nexus", User: user, IsAdmin: false, - RefreshTokens: new Dictionary() + PersonalAccessTokens: new Dictionary() ); return Task.FromResult(meResponse); } - public IReadOnlyDictionary GetRefreshTokens(string userId) + public IReadOnlyDictionary GetTokens(string userId) { throw new NotImplementedException(); } - public Task> GetRefreshTokensAsync(string userId, CancellationToken cancellationToken = default) + public Task> GetTokensAsync(string userId, CancellationToken cancellationToken = default) { throw new NotImplementedException(); } @@ -487,26 +492,6 @@ public Task> GetUsersAsync(CancellationTo throw new NotImplementedException(); } - public TokenPair RefreshToken(RefreshTokenRequest request) - { - throw new NotImplementedException(); - } - - public Task RefreshTokenAsync(RefreshTokenRequest request, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public HttpResponseMessage RevokeToken(RevokeTokenRequest request) - { - throw new NotImplementedException(); - } - - public Task RevokeTokenAsync(RevokeTokenRequest request, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - public void SignOut(string returnUrl) { throw new NotImplementedException(); diff --git a/src/Nexus/API/SourcesController.cs b/src/Nexus/API/SourcesController.cs index 61db539c..c4595b81 100644 --- a/src/Nexus/API/SourcesController.cs +++ b/src/Nexus/API/SourcesController.cs @@ -96,7 +96,7 @@ public ActionResult> GetRegistrations( /// The optional user identifier. If not specified, the current user will be used. [HttpPost("registrations")] public async Task> CreateRegistrationAsync( - [FromBody] DataSourceRegistration registration, + DataSourceRegistration registration, [FromQuery] string? userId = default) { if (TryAuthenticate(userId, out var actualUserId, out var response)) diff --git a/src/Nexus/API/UsersController.cs b/src/Nexus/API/UsersController.cs index 3ce15b8b..6a308203 100644 --- a/src/Nexus/API/UsersController.cs +++ b/src/Nexus/API/UsersController.cs @@ -177,12 +177,12 @@ public async Task> GetMeAsync() /// /// Creates a personal access token. /// - /// The create token request. + /// 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( - CreateTokenRequest request, + PersonalAccessToken token, [FromQuery] string? userId = default ) { @@ -193,10 +193,14 @@ public async Task> CreateTokenAsync( if (user is null) return NotFound($"Could not find user {userId}."); - await _tokenService - .CreateAsync(actualUserId, request.Description, request.Expires, request.Claims); + var tokenValue = await _tokenService + .CreateAsync( + actualUserId, + token.Description, + token.Expires, + token.Claims); - return Ok(); + return Ok(tokenValue); } else diff --git a/src/Nexus/Core/Models_Public.cs b/src/Nexus/Core/Models_Public.cs index 17d0deda..9b3e80cf 100644 --- a/src/Nexus/Core/Models_Public.cs +++ b/src/Nexus/Core/Models_Public.cs @@ -94,17 +94,6 @@ public record PersonalAccessToken( IReadOnlyList Claims ); - /// - /// A revoke token request. - /// - /// The token description. - /// The date/time when the token expires. - /// The claims that will be part of the token. - public record CreateTokenRequest( - string Description, - DateTime Expires, - IReadOnlyList Claims); - /// /// A revoke token request. /// diff --git a/src/Nexus/Core/NexusAuthExtensions.cs b/src/Nexus/Core/NexusAuthExtensions.cs index 38608708..c1562f57 100644 --- a/src/Nexus/Core/NexusAuthExtensions.cs +++ b/src/Nexus/Core/NexusAuthExtensions.cs @@ -1,10 +1,10 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; -using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.DataProtection; using Microsoft.EntityFrameworkCore; +using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Nexus.Core; using Nexus.Utilities; @@ -32,6 +32,8 @@ public static IServiceCollection AddNexusAuth( { /* https://stackoverflow.com/a/52493428/1636629 */ + JsonWebTokenHandler.DefaultInboundClaimTypeMap.Clear(); + services.AddDataProtection() .PersistKeysToFileSystem(new DirectoryInfo(Path.Combine(pathsOptions.Config, "data-protection-keys"))); @@ -205,7 +207,7 @@ public static IServiceCollection AddNexusAuth( var authenticationSchemes = new[] { CookieAuthenticationDefaults.AuthenticationScheme, - JwtBearerDefaults.AuthenticationScheme + // JwtBearerDefaults.AuthenticationScheme }; services.AddAuthorization(options => @@ -218,8 +220,8 @@ public static IServiceCollection AddNexusAuth( options .AddPolicy(NexusPolicies.RequireAdmin, policy => policy - .RequireRole(NexusRoles.ADMINISTRATOR) - .AddAuthenticationSchemes(authenticationSchemes)); + .RequireRole(NexusRoles.ADMINISTRATOR) + .AddAuthenticationSchemes(authenticationSchemes)); }); return services; diff --git a/src/Nexus/Services/DatabaseService.cs b/src/Nexus/Services/DatabaseService.cs index 3d843179..5f686d77 100644 --- a/src/Nexus/Services/DatabaseService.cs +++ b/src/Nexus/Services/DatabaseService.cs @@ -341,7 +341,7 @@ public bool TryReadTokenMap( string userId, [NotNullWhen(true)] out string? tokenMap) { - var folderPath = Path.Combine(SafePathCombine(_pathsOptions.Users, userId), "tokens"); + var folderPath = SafePathCombine(_pathsOptions.Users, userId); var tokenFilePath = Path.Combine(folderPath, "tokens.json"); tokenMap = default; @@ -358,7 +358,7 @@ public bool TryReadTokenMap( public Stream WriteTokenMap( string userId) { - var folderPath = Path.Combine(SafePathCombine(_pathsOptions.Users, userId), "tokens"); + var folderPath = SafePathCombine(_pathsOptions.Users, userId); var tokenFilePath = Path.Combine(folderPath, "tokens.json"); Directory.CreateDirectory(folderPath); diff --git a/src/clients/dotnet-client/NexusClient.g.cs b/src/clients/dotnet-client/NexusClient.g.cs index cb21e0f9..cbdc1f73 100644 --- a/src/clients/dotnet-client/NexusClient.g.cs +++ b/src/clients/dotnet-client/NexusClient.g.cs @@ -2231,16 +2231,16 @@ public interface IUsersClient /// Creates a personal access token. ///
/// The optional user identifier. If not specified, the current user will be used. - /// The create token request. - string CreateToken(CreateTokenRequest request, string? userId = default); + /// The personal access token to create. + string CreateToken(PersonalAccessToken token, string? userId = default); /// /// Creates a personal access token. /// /// The optional user identifier. If not specified, the current user will be used. - /// The create token request. + /// The personal access token to create. /// The token to cancel the current operation. - Task CreateTokenAsync(CreateTokenRequest request, string? userId = default, CancellationToken cancellationToken = default); + Task CreateTokenAsync(PersonalAccessToken token, string? userId = default, CancellationToken cancellationToken = default); /// /// Deletes a personal access token. @@ -2518,7 +2518,7 @@ public Task GetMeAsync(CancellationToken cancellationToken = default } /// - public string CreateToken(CreateTokenRequest request, string? userId = default) + public string CreateToken(PersonalAccessToken token, string? userId = default) { var __urlBuilder = new StringBuilder(); __urlBuilder.Append("/api/v1/users/tokens/create"); @@ -2532,11 +2532,11 @@ public string CreateToken(CreateTokenRequest request, string? userId = default) __urlBuilder.Append(__query); var __url = __urlBuilder.ToString(); - return ___client.Invoke("POST", __url, "application/json", "application/json", JsonContent.Create(request, options: Utilities.JsonOptions)); + return ___client.Invoke("POST", __url, "application/json", "application/json", JsonContent.Create(token, options: Utilities.JsonOptions)); } /// - public Task CreateTokenAsync(CreateTokenRequest request, string? userId = default, CancellationToken cancellationToken = default) + public Task CreateTokenAsync(PersonalAccessToken token, string? userId = default, CancellationToken cancellationToken = default) { var __urlBuilder = new StringBuilder(); __urlBuilder.Append("/api/v1/users/tokens/create"); @@ -2550,7 +2550,7 @@ public Task CreateTokenAsync(CreateTokenRequest request, string? userId __urlBuilder.Append(__query); var __url = __urlBuilder.ToString(); - return ___client.InvokeAsync("POST", __url, "application/json", "application/json", JsonContent.Create(request, options: Utilities.JsonOptions), cancellationToken); + return ___client.InvokeAsync("POST", __url, "application/json", "application/json", JsonContent.Create(token, options: Utilities.JsonOptions), cancellationToken); } /// @@ -3142,14 +3142,6 @@ public record PersonalAccessToken(string Description, DateTime Expires, IReadOnl /// The claim value. public record TokenClaim(string Type, string Value); -/// -/// A revoke token request. -/// -/// The token description. -/// The date/time when the token expires. -/// The claims that will be part of the token. -public record CreateTokenRequest(string Description, DateTime Expires, IReadOnlyList Claims); - /// /// Represents a claim. /// diff --git a/src/clients/matlab-client/NexusClient.m b/src/clients/matlab-client/NexusClient.m index fbc40760..f27f3653 100644 --- a/src/clients/matlab-client/NexusClient.m +++ b/src/clients/matlab-client/NexusClient.m @@ -2,10 +2,6 @@ properties(Access = private) BaseUrl - AccessToken - TokenPair - TokenFolderPath - TokenFilePath AuthorizationHeader ConfigurationHeader end @@ -13,46 +9,11 @@ methods function self = NexusClient(baseUrl) - - self.BaseUrl = baseUrl; - - if ispc - home = getenv('USERPROFILE'); - else - home = getenv('HOME'); - end - - self.TokenFolderPath = fullfile(home, '.nexus-api', 'tokens'); + self.BaseUrl = baseUrl; end - function signIn(self, refreshToken) - - import java.lang.String - import java.math.* - import java.security.* - - sha256 = MessageDigest.getInstance('sha-256'); - hash = sha256.digest(double(refreshToken)); - bigInteger = BigInteger(1, hash); - refreshTokenHash = char(String.format('%064x', bigInteger)); - self.TokenFilePath = fullfile(self.TokenFolderPath, [refreshTokenHash '.json']); - - if isfile(self.TokenFilePath) - actualRefreshToken = fileread(self.TokenFilePath); - - else - if ~exist(self.TokenFolderPath, 'dir') - mkdir(self.TokenFolderPath) - end - - fileId = fopen(self.TokenFilePath, 'w'); - fprintf(fileId, refreshToken); - fclose(fileId); - - actualRefreshToken = refreshToken; - end - - self.refreshToken(actualRefreshToken) + function signIn(self, accessToken) + self.AuthorizationHeader = GenericField('Authorization', ['Bearer ' accessToken]); end function attachConfiguration(self, configuration) @@ -193,19 +154,9 @@ function export(self, dateTimeBegin, dateTimeEnd, filePeriod, fileFormat, ... import matlab.net.http.* import matlab.net.http.field.* - options = weboptions('HeaderFields', self.AuthorizationHeader); - - try - websave(tmpFilePath, downloadUrl, options); - catch - try - self.refreshToken(self.TokenPair.RefreshToken) - websave(tmpFilePath, downloadUrl, options); - catch - % do nothing - end - end - + options = weboptions('HeaderFields', self.AuthorizationHeader); + + websave(tmpFilePath, downloadUrl, options); onProgress(1, 'download') % Extract file @@ -276,16 +227,6 @@ function export(self, dateTimeBegin, dateTimeEnd, filePeriod, fileFormat, ... downloadUrl = URI([self.BaseUrl sprintf('/api/v1/artifacts/%s', artifactId)]); end - function tokenPair = users_refreshToken(self, refreshToken) - import matlab.net.* - import matlab.net.http.* - - requestMessage = RequestMessage('post', [], refreshToken); - uri = URI([self.BaseUrl '/api/v1/users/tokens/refresh']); - response = self.send(requestMessage, uri); - tokenPair = self.toPascalCase(response.Body.Data); - end - function response = send(self, requestMessage, uri) import matlab.net.http.* @@ -297,71 +238,11 @@ function export(self, dateTimeBegin, dateTimeEnd, filePeriod, fileFormat, ... % process response if ~self.isSuccessStatusCode(response) - - if response.StatusCode == 401 && ~isempty(self.TokenPair) - wwwAuthenticateHeader = response.Header.getFields('WWW-Authenticate'); - signOut = true; - - if ~isempty(wwwAuthenticateHeader) - - if contains(wwwAuthenticateHeader.Value, 'The token expired at') - - try - self.refreshToken(self.TokenPair.RefreshToken) - - requestMessage.Header = [requestMessage.Header self.AuthorizationHeader self.ConfigurationHeader]; - options = HTTPOptions('ResponseTimeout', 60); - newResponse = requestMessage.send(uri, options); - response = newResponse; - signOut = false; - catch - % do nothing - end - end - end - - if signOut - self.signOut() - end - end - if ~self.isSuccessStatusCode(response) - message = [char(response.StatusLine.ReasonPhrase) ' - ' char(response.Body.Data)]; - error('The HTTP request failed with status code %d. The response message is: %s', response.StatusCode, message) - end + message = [char(response.StatusLine.ReasonPhrase) ' - ' char(response.Body.Data)]; + error('The HTTP request failed with status code %d. The response message is: %s', response.StatusCode, message) end end - - function signOut(self) - self.AuthorizationHeader = []; - self.TokenPair = []; - end - - function refreshToken(self, refreshToken) - - import matlab.net.http.field.* - - % make sure the refresh token has not already been redeemed - if ~isempty(self.TokenPair) && strcmp(refreshToken, self.TokenPair.RefreshToken) == 0 - return - end - - refreshRequest = struct('refreshToken', refreshToken); - tokenPair = self.users_refreshToken(refreshRequest); - - if ~isempty(self.TokenFilePath) - if ~exist(self.TokenFolderPath, 'dir') - mkdir(self.TokenFolderPath) - end - - fileId = fopen(self.TokenFilePath, 'w'); - fprintf(fileId, tokenPair.RefreshToken); - fclose(fileId); - end - - self.AuthorizationHeader = GenericField('Authorization', ['Bearer ' tokenPair.AccessToken]); - self.TokenPair = tokenPair; - end function result = isSuccessStatusCode(~, response) result = 200 <= response.StatusCode && response.StatusCode < 300; diff --git a/src/clients/python-client/nexus_api/_nexus_api.py b/src/clients/python-client/nexus_api/_nexus_api.py index 367d2430..29b217bb 100644 --- a/src/clients/python-client/nexus_api/_nexus_api.py +++ b/src/clients/python-client/nexus_api/_nexus_api.py @@ -789,27 +789,6 @@ class TokenClaim: """The claim value.""" -@dataclass(frozen=True) -class CreateTokenRequest: - """ - A revoke token request. - - Args: - description: The token description. - expires: The date/time when the token expires. - claims: The claims that will be part of the token. - """ - - description: str - """The token description.""" - - expires: datetime - """The date/time when the token expires.""" - - claims: list[TokenClaim] - """The claims that will be part of the token.""" - - @dataclass(frozen=True) class NexusClaim: """ @@ -1442,7 +1421,7 @@ def get_me(self) -> Awaitable[MeResponse]: return self.___client._invoke(MeResponse, "GET", __url, "application/json", None, None) - def create_token(self, request: CreateTokenRequest, user_id: Optional[str] = None) -> Awaitable[str]: + def create_token(self, token: PersonalAccessToken, user_id: Optional[str] = None) -> Awaitable[str]: """ Creates a personal access token. @@ -1460,7 +1439,7 @@ def create_token(self, request: CreateTokenRequest, user_id: Optional[str] = Non __query: str = "?" + "&".join(f"{key}={value}" for (key, value) in __query_values.items()) __url += __query - return self.___client._invoke(str, "POST", __url, "application/json", "application/json", json.dumps(JsonEncoder.encode(request, _json_encoder_options))) + return self.___client._invoke(str, "POST", __url, "application/json", "application/json", json.dumps(JsonEncoder.encode(token, _json_encoder_options))) def delete_token(self, token_id: UUID) -> Awaitable[Response]: """ @@ -2217,7 +2196,7 @@ def get_me(self) -> MeResponse: return self.___client._invoke(MeResponse, "GET", __url, "application/json", None, None) - def create_token(self, request: CreateTokenRequest, user_id: Optional[str] = None) -> str: + def create_token(self, token: PersonalAccessToken, user_id: Optional[str] = None) -> str: """ Creates a personal access token. @@ -2235,7 +2214,7 @@ def create_token(self, request: CreateTokenRequest, user_id: Optional[str] = Non __query: str = "?" + "&".join(f"{key}={value}" for (key, value) in __query_values.items()) __url += __query - return self.___client._invoke(str, "POST", __url, "application/json", "application/json", json.dumps(JsonEncoder.encode(request, _json_encoder_options))) + return self.___client._invoke(str, "POST", __url, "application/json", "application/json", json.dumps(JsonEncoder.encode(token, _json_encoder_options))) def delete_token(self, token_id: UUID) -> Response: """ diff --git a/tests/clients/python-client-tests/async-client-tests.py b/tests/clients/python-client-tests/async-client-tests.py index 816f1d3b..b167ef8d 100644 --- a/tests/clients/python-client-tests/async-client-tests.py +++ b/tests/clients/python-client-tests/async-client-tests.py @@ -36,13 +36,6 @@ def _handler(request: Request): catalog_json_string = '{"Id":"my-catalog-id","Properties":null,"Resources":null}' return Response(codes.OK, content=catalog_json_string) - elif "refresh-token" in request.url.path: - requestContent = request.content.decode("utf-8") - assert '{"refreshToken": "456"}' == requestContent - - new_token_pair_json_string = '{ "accessToken": "123", "refreshToken": "456" }' - return Response(codes.OK, content=new_token_pair_json_string) - else: raise Exception("Unsupported path.") diff --git a/tests/clients/python-client-tests/sync-client-tests.py b/tests/clients/python-client-tests/sync-client-tests.py index 312787ee..e3e93b26 100644 --- a/tests/clients/python-client-tests/sync-client-tests.py +++ b/tests/clients/python-client-tests/sync-client-tests.py @@ -1,9 +1,8 @@ import base64 import json -import uuid from httpx import Client, MockTransport, Request, Response, codes -from nexus_api import NexusClient, ResourceCatalog +from nexus_api import NexusClient nexus_configuration_header_key = "Nexus-Configuration" @@ -36,13 +35,6 @@ def _handler(request: Request): catalog_json_string = '{"Id":"my-catalog-id","Properties":null,"Resources":null}' return Response(codes.OK, content=catalog_json_string) - elif "refresh-token" in request.url.path: - requestContent = request.content.decode("utf-8") - assert '{"refreshToken": "456"}' == requestContent - - new_token_pair_json_string = '{ "accessToken": "123", "refreshToken": "456" }' - return Response(codes.OK, content=new_token_pair_json_string) - else: raise Exception("Unsupported path.") From 9d8f61e5a9f4a7e5a556f74214689382481909fe Mon Sep 17 00:00:00 2001 From: Apollo3zehn Date: Thu, 7 Mar 2024 19:05:00 +0100 Subject: [PATCH 11/19] Token works, but is too powerful right now --- src/Nexus/API/CatalogsController.cs | 18 +++- src/Nexus/Core/NexusAuthExtensions.cs | 7 +- ...ersonalAccessTokenAuthenticationHandler.cs | 89 +++++++++++++++++++ src/Nexus/Services/TokenService.cs | 66 +++++++++----- src/Nexus/Utilities/AuthorizationUtilities.cs | 11 ++- 5 files changed, 162 insertions(+), 29 deletions(-) create mode 100644 src/Nexus/Core/PersonalAccessTokenAuthenticationHandler.cs diff --git a/src/Nexus/API/CatalogsController.cs b/src/Nexus/API/CatalogsController.cs index a9e6dea0..81e8c171 100644 --- a/src/Nexus/API/CatalogsController.cs +++ b/src/Nexus/API/CatalogsController.cs @@ -528,11 +528,21 @@ private async Task> ProtectCatalogAsync( if (catalogContainer is not null) { - if (ensureReadable && !AuthorizationUtilities.IsCatalogReadable(catalogContainer.Id, catalogContainer.Metadata, catalogContainer.Owner, User)) - return StatusCode(StatusCodes.Status403Forbidden, $"The current user is not permitted to read the catalog {catalogId}."); + if (ensureReadable && !AuthorizationUtilities.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 && !AuthorizationUtilities.IsCatalogWritable(catalogContainer.Id, catalogContainer.Metadata, User)) - return StatusCode(StatusCodes.Status403Forbidden, $"The current user is not permitted to modify the catalog {catalogId}."); + if (ensureWritable && !AuthorizationUtilities.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); } diff --git a/src/Nexus/Core/NexusAuthExtensions.cs b/src/Nexus/Core/NexusAuthExtensions.cs index c1562f57..4e1907ac 100644 --- a/src/Nexus/Core/NexusAuthExtensions.cs +++ b/src/Nexus/Core/NexusAuthExtensions.cs @@ -54,7 +54,10 @@ public static IServiceCollection AddNexusAuth( context.Response.StatusCode = (int)HttpStatusCode.Forbidden; return Task.CompletedTask; }; - }); + }) + + .AddScheme( + PersonalAccessTokenAuthenticationDefaults.AuthenticationScheme, default); var providers = securityOptions.OidcProviders.Any() ? securityOptions.OidcProviders @@ -207,7 +210,7 @@ public static IServiceCollection AddNexusAuth( var authenticationSchemes = new[] { CookieAuthenticationDefaults.AuthenticationScheme, - // JwtBearerDefaults.AuthenticationScheme + PersonalAccessTokenAuthenticationDefaults.AuthenticationScheme }; services.AddAuthorization(options => diff --git a/src/Nexus/Core/PersonalAccessTokenAuthenticationHandler.cs b/src/Nexus/Core/PersonalAccessTokenAuthenticationHandler.cs new file mode 100644 index 00000000..0345b590 --- /dev/null +++ b/src/Nexus/Core/PersonalAccessTokenAuthenticationHandler.cs @@ -0,0 +1,89 @@ +using System.Security.Claims; +using System.Text.Encodings.Web; +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Options; +using Nexus.Services; +using static OpenIddict.Abstractions.OpenIddictConstants; + +namespace Nexus.Core; + +internal static class PersonalAccessTokenAuthenticationDefaults +{ + public const string AuthenticationScheme = "pat"; +} + +internal class PersonalAccessTokenAuthHandler : AuthenticationHandler +{ + private readonly ITokenService _tokenService; + + public PersonalAccessTokenAuthHandler( + ITokenService tokenService, + IOptionsMonitor options, + ILoggerFactory logger, + UrlEncoder encoder) : base(options, logger, encoder) + { + _tokenService = tokenService; + } + + protected override Task HandleAuthenticateAsync() + { + var headerValues = Request.Headers.Authorization; + var principal = default(ClaimsPrincipal); + + foreach (var headerValue in headerValues) + { + if (headerValue is null) + continue; + + if (headerValue.StartsWith("bearer ", StringComparison.OrdinalIgnoreCase)) + { + var parts = headerValue.Split(' ', count: 2); + var tokenValue = parts[1]; + + if (_tokenService.TryGet(tokenValue, out var userId, out var token)) + { + var claims = token.Claims + + .Where(claim => + claim.Type == NexusClaims.CAN_READ_CATALOG || + claim.Type == NexusClaims.CAN_WRITE_CATALOG) + + /* Prefix PAT claims with "pat_" so they are distinguishable from the + * more powerful user claims. It will be checked for in the catalogs + * controller. + */ + .Select(claim => new Claim($"pat_{claim.Type}", claim.Value)); + + claims = claims.Append(new Claim(Claims.Subject, userId)); + claims = claims.Append(new Claim(Claims.Role, NexusRoles.USER)); + + var identity = new ClaimsIdentity( + claims, + Scheme.Name, + nameType: Claims.Name, + roleType: Claims.Role); + + if (principal is null) + principal = new ClaimsPrincipal(); + + principal.AddIdentity(identity); + } + } + } + + AuthenticateResult result; + + if (principal is null) + { + result = AuthenticateResult.NoResult(); + } + + else + { + var ticket = new AuthenticationTicket(principal, Scheme.Name); + result = AuthenticateResult.Success(ticket); + } + + return Task.FromResult(result); + } +} \ No newline at end of file diff --git a/src/Nexus/Services/TokenService.cs b/src/Nexus/Services/TokenService.cs index f4c491c6..b43fa4a5 100644 --- a/src/Nexus/Services/TokenService.cs +++ b/src/Nexus/Services/TokenService.cs @@ -1,4 +1,5 @@ using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; using System.Security.Cryptography; using System.Text.Json; using Nexus.Core; @@ -14,6 +15,11 @@ Task CreateAsync( DateTime expires, IReadOnlyList claims); + bool TryGet( + string tokenValue, + [NotNullWhen(true)] out string? userId, + [NotNullWhen(true)] out InternalPersonalAccessToken? token); + Task DeleteAsync( string userId, Guid tokenId); @@ -46,7 +52,7 @@ public Task CreateAsync( DateTime expires, IReadOnlyList claims) { - return UpdateTokenMapAsync(userId, tokenMap => + return InteractWithTokenMapAsync(userId, tokenMap => { var id = Guid.NewGuid(); @@ -76,7 +82,7 @@ public Task CreateAsync( public Task DeleteAsync(string userId, Guid tokenId) { - return UpdateTokenMapAsync(userId, tokenMap => + return InteractWithTokenMapAsync(userId, tokenMap => { var tokenEntry = tokenMap .FirstOrDefault(entry => entry.Value.Id == tokenId); @@ -92,7 +98,7 @@ public Task DeleteAsync(string tokenValue) var secret = splittedTokenValue[0]; var userId = splittedTokenValue[1]; - return UpdateTokenMapAsync(userId, tokenMap => + return InteractWithTokenMapAsync(userId, tokenMap => { tokenMap.TryRemove(secret, out _); return default; @@ -102,13 +108,46 @@ public Task DeleteAsync(string tokenValue) public Task> GetAllAsync( string userId) { - return UpdateTokenMapAsync( + return InteractWithTokenMapAsync( userId, tokenMap => (IReadOnlyDictionary)tokenMap, saveChanges: false); } - private async Task UpdateTokenMapAsync( + public bool TryGet( + string tokenValue, + [NotNullWhen(true)] out string? userId, + [NotNullWhen(true)] out InternalPersonalAccessToken? token) + { + var splittedTokenValue = tokenValue.Split('_', count: 2); + var secret = splittedTokenValue[0]; + userId = splittedTokenValue[1]; + var tokenMap = GetTokenMap(userId); + + return tokenMap.TryGetValue(secret, out token); + } + + private ConcurrentDictionary GetTokenMap( + string userId) + { + return _cache.GetOrAdd( + userId, + key => + { + if (_databaseService.TryReadTokenMap(userId, out var jsonString)) + { + return JsonSerializer.Deserialize>(jsonString) + ?? throw new Exception("tokenMap is null"); + } + + else + { + return new ConcurrentDictionary(); + } + }); + } + + private async Task InteractWithTokenMapAsync( string userId, Func, T> func, bool saveChanges) @@ -117,22 +156,7 @@ private async Task UpdateTokenMapAsync( try { - var tokenMap = _cache.GetOrAdd( - userId, - key => - { - if (_databaseService.TryReadTokenMap(userId, out var jsonString)) - { - return JsonSerializer.Deserialize>(jsonString) - ?? throw new Exception("tokenMap is null"); - } - - else - { - return new ConcurrentDictionary(); - } - }); - + var tokenMap = GetTokenMap(userId); var result = func(tokenMap); if (saveChanges) diff --git a/src/Nexus/Utilities/AuthorizationUtilities.cs b/src/Nexus/Utilities/AuthorizationUtilities.cs index 0af6b1c0..2dc43349 100644 --- a/src/Nexus/Utilities/AuthorizationUtilities.cs +++ b/src/Nexus/Utilities/AuthorizationUtilities.cs @@ -8,7 +8,11 @@ namespace Nexus.Utilities { internal static class AuthorizationUtilities { - public static bool IsCatalogReadable(string catalogId, CatalogMetadata catalogMetadata, ClaimsPrincipal? owner, ClaimsPrincipal user) + public static bool IsCatalogReadable( + string catalogId, + CatalogMetadata catalogMetadata, + ClaimsPrincipal? owner, + ClaimsPrincipal user) { var identity = user.Identity; @@ -38,7 +42,10 @@ public static bool IsCatalogReadable(string catalogId, CatalogMetadata catalogMe return false; } - public static bool IsCatalogWritable(string catalogId, CatalogMetadata catalogMetadata, ClaimsPrincipal user) + public static bool IsCatalogWritable( + string catalogId, + CatalogMetadata catalogMetadata, + ClaimsPrincipal user) { var identity = user.Identity; From 9c86b779013c5aa977da25b7eb3e5ee94144497e Mon Sep 17 00:00:00 2001 From: Apollo3zehn Date: Thu, 7 Mar 2024 19:05:00 +0100 Subject: [PATCH 12/19] PAT auth works now --- .../Components/UserSettingsView.razor | 5 +- src/Nexus/API/CatalogsController.cs | 16 +- src/Nexus/API/JobsController.cs | 4 +- src/Nexus/API/UsersController.cs | 13 +- src/Nexus/Core/CatalogContainer.cs | 2 +- src/Nexus/Core/NexusClaims.cs | 5 + ...ersonalAccessTokenAuthenticationHandler.cs | 50 ++++-- src/Nexus/Services/CatalogManager.cs | 4 +- src/Nexus/Services/DataService.cs | 2 +- src/Nexus/Services/TokenService.cs | 47 ++--- src/Nexus/Utilities/AuthUtilities.cs | 170 ++++++++++++++++++ src/Nexus/Utilities/AuthorizationUtilities.cs | 70 -------- .../Other/PackageControllerTests.cs | 2 +- tests/Nexus.Tests/Other/UtilitiesTests.cs | 8 +- .../Services/DataControllerServiceTests.cs | 2 +- .../Nexus.Tests/Services/TokenServiceTests.cs | 35 +++- 16 files changed, 292 insertions(+), 143 deletions(-) create mode 100644 src/Nexus/Utilities/AuthUtilities.cs delete mode 100644 src/Nexus/Utilities/AuthorizationUtilities.cs diff --git a/src/Nexus.UI/Components/UserSettingsView.razor b/src/Nexus.UI/Components/UserSettingsView.razor index 6bba395a..dea7baff 100644 --- a/src/Nexus.UI/Components/UserSettingsView.razor +++ b/src/Nexus.UI/Components/UserSettingsView.razor @@ -340,18 +340,21 @@ try { var claims = _newAccessTokenClaims + .Where( claimData => claimData is not null && claimData.CatalogPattern is not null) + .Select(claimData => new TokenClaim( claimData.WriteAccess ? "CanWriteCatalog" : "CanReadCatalog", claimData.CatalogPattern!)) + .ToList(); var token = new PersonalAccessToken( _newAccessTokenDescription, _newAccessTokenExpiration.HasValue - ? _newAccessTokenExpiration.Value + ? _newAccessTokenExpiration.Value.ToUniversalTime() : DateTime.MaxValue, claims ); diff --git a/src/Nexus/API/CatalogsController.cs b/src/Nexus/API/CatalogsController.cs index 81e8c171..b4f91a09 100644 --- a/src/Nexus/API/CatalogsController.cs +++ b/src/Nexus/API/CatalogsController.cs @@ -101,7 +101,7 @@ public async Task>> { var catalogContainer = group.First().Request.Container; - if (!AuthorizationUtilities.IsCatalogReadable(catalogContainer.Id, catalogContainer.Metadata, catalogContainer.Owner, User)) + if (!AuthUtilities.IsCatalogReadable(catalogContainer.Id, catalogContainer.Metadata, catalogContainer.Owner, User)) throw new UnauthorizedAccessException($"The current user is not permitted to access catalog {catalogContainer.Id}."); } } @@ -182,8 +182,8 @@ public async Task> license = reader.ReadToEnd(); } - var isReadable = AuthorizationUtilities.IsCatalogReadable(childContainer.Id, childContainer.Metadata, childContainer.Owner, User); - var isWritable = AuthorizationUtilities.IsCatalogWritable(childContainer.Id, childContainer.Metadata, User); + 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 ?? ""); @@ -499,7 +499,7 @@ public Task var response = ProtectCatalogAsync(catalogId, ensureReadable: true, ensureWritable: false, async catalogContainer => { - var canEdit = AuthorizationUtilities.IsCatalogWritable(catalogId, catalogContainer.Metadata, User); + 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}."); @@ -528,7 +528,7 @@ private async Task> ProtectCatalogAsync( if (catalogContainer is not null) { - if (ensureReadable && !AuthorizationUtilities.IsCatalogReadable( + if (ensureReadable && !AuthUtilities.IsCatalogReadable( catalogContainer.Id, catalogContainer.Metadata, catalogContainer.Owner, User)) { return StatusCode( @@ -536,7 +536,7 @@ private async Task> ProtectCatalogAsync( $"The current user is not permitted to read the catalog {catalogId}."); } - if (ensureWritable && !AuthorizationUtilities.IsCatalogWritable( + if (ensureWritable && !AuthUtilities.IsCatalogWritable( catalogContainer.Id, catalogContainer.Metadata, User)) { return StatusCode( @@ -564,10 +564,10 @@ private async Task ProtectCatalogNonGenericAsync( if (catalogContainer is not null) { - if (ensureReadable && !AuthorizationUtilities.IsCatalogReadable(catalogContainer.Id, catalogContainer.Metadata, catalogContainer.Owner, User)) + 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 && !AuthorizationUtilities.IsCatalogWritable(catalogContainer.Id, catalogContainer.Metadata, User)) + if (ensureWritable && !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); diff --git a/src/Nexus/API/JobsController.cs b/src/Nexus/API/JobsController.cs index f7648f4f..02ba3c83 100644 --- a/src/Nexus/API/JobsController.cs +++ b/src/Nexus/API/JobsController.cs @@ -208,7 +208,7 @@ public async Task> ExportAsync( { var catalogContainer = group.First().Container; - if (!AuthorizationUtilities.IsCatalogReadable(catalogContainer.Id, catalogContainer.Metadata, catalogContainer.Owner, User)) + if (!AuthUtilities.IsCatalogReadable(catalogContainer.Id, catalogContainer.Metadata, catalogContainer.Owner, User)) throw new UnauthorizedAccessException($"The current user is not permitted to access catalog {catalogContainer.Id}."); } } @@ -336,7 +336,7 @@ private async Task ProtectCatalogNonGenericAsync( if (catalogContainer is not null) { - if (!AuthorizationUtilities.IsCatalogWritable(catalogContainer.Id, catalogContainer.Metadata, User)) + 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); diff --git a/src/Nexus/API/UsersController.cs b/src/Nexus/API/UsersController.cs index 6a308203..e54834d2 100644 --- a/src/Nexus/API/UsersController.cs +++ b/src/Nexus/API/UsersController.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.Options; using Nexus.Core; using Nexus.Services; +using Nexus.Utilities; using System.Diagnostics.CodeAnalysis; using System.Net; using System.Security.Claims; @@ -134,7 +135,9 @@ public async Task SignOutAsync( public async Task DeleteTokenByValueAsync( [BindRequired] string value) { - await _tokenService.DeleteAsync(value); + var (userId, secret) = AuthUtilities.TokenValueToComponents(value); + await _tokenService.DeleteAsync(userId, secret); + return Ok(); } @@ -193,13 +196,17 @@ public async Task> CreateTokenAsync( if (user is null) return NotFound($"Could not find user {userId}."); - var tokenValue = await _tokenService + var utcExpires = token.Expires.ToUniversalTime(); + + var secret = await _tokenService .CreateAsync( actualUserId, token.Description, - token.Expires, + utcExpires, token.Claims); + var tokenValue = AuthUtilities.ComponentsToTokenValue(actualUserId, secret); + return Ok(tokenValue); } diff --git a/src/Nexus/Core/CatalogContainer.cs b/src/Nexus/Core/CatalogContainer.cs index fe1c606d..3e6eb5cf 100644 --- a/src/Nexus/Core/CatalogContainer.cs +++ b/src/Nexus/Core/CatalogContainer.cs @@ -41,7 +41,7 @@ public CatalogContainer( _dataControllerService = dataControllerService; if (owner is not null) - IsReleasable = AuthorizationUtilities.IsCatalogWritable(Id, metadata, owner); + IsReleasable = AuthUtilities.IsCatalogWritable(Id, metadata, owner); } public string Id { get; } diff --git a/src/Nexus/Core/NexusClaims.cs b/src/Nexus/Core/NexusClaims.cs index 59a1c0bc..ae112b69 100644 --- a/src/Nexus/Core/NexusClaims.cs +++ b/src/Nexus/Core/NexusClaims.cs @@ -6,5 +6,10 @@ internal static class NexusClaims 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}"; + } } } \ No newline at end of file diff --git a/src/Nexus/Core/PersonalAccessTokenAuthenticationHandler.cs b/src/Nexus/Core/PersonalAccessTokenAuthenticationHandler.cs index 0345b590..670eaf17 100644 --- a/src/Nexus/Core/PersonalAccessTokenAuthenticationHandler.cs +++ b/src/Nexus/Core/PersonalAccessTokenAuthenticationHandler.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.Options; using Nexus.Services; +using Nexus.Utilities; using static OpenIddict.Abstractions.OpenIddictConstants; namespace Nexus.Core; @@ -16,16 +17,20 @@ internal class PersonalAccessTokenAuthHandler : AuthenticationHandler options, ILoggerFactory logger, UrlEncoder encoder) : base(options, logger, encoder) { _tokenService = tokenService; + _dbService = dbService; } - protected override Task HandleAuthenticateAsync() + protected async override Task HandleAuthenticateAsync() { var headerValues = Request.Headers.Authorization; var principal = default(ClaimsPrincipal); @@ -35,30 +40,41 @@ protected override Task HandleAuthenticateAsync() if (headerValue is null) continue; - if (headerValue.StartsWith("bearer ", StringComparison.OrdinalIgnoreCase)) + if (headerValue.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) { var parts = headerValue.Split(' ', count: 2); - var tokenValue = parts[1]; + var (userId, secret) = AuthUtilities.TokenValueToComponents(parts[1]); + + var user = await _dbService.FindUserAsync(userId); - if (_tokenService.TryGet(tokenValue, out var userId, out var token)) + if (user is null) + continue; + + if (_tokenService.TryGet(userId, secret, out var token)) { - var claims = token.Claims + if (DateTime.UtcNow >= token.Expires) + return AuthenticateResult.NoResult(); + + var userClaims = user.Claims + .Select(claim => new Claim(NexusClaims.ToPatUserClaimType(claim.Type), claim.Value)); - .Where(claim => - claim.Type == NexusClaims.CAN_READ_CATALOG || - claim.Type == NexusClaims.CAN_WRITE_CATALOG) + var tokenClaimsRead = token.Claims + .Where(claim => claim.Type == NexusClaims.CAN_READ_CATALOG) + .Select(claim => new Claim(NexusClaims.CAN_READ_CATALOG, claim.Value)); - /* Prefix PAT claims with "pat_" so they are distinguishable from the - * more powerful user claims. It will be checked for in the catalogs - * controller. - */ - .Select(claim => new Claim($"pat_{claim.Type}", claim.Value)); + var tokenClaimsWrite = token.Claims + .Where(claim => claim.Type == NexusClaims.CAN_WRITE_CATALOG) + .Select(claim => new Claim(NexusClaims.CAN_WRITE_CATALOG, claim.Value)); - claims = claims.Append(new Claim(Claims.Subject, userId)); - claims = claims.Append(new Claim(Claims.Role, NexusRoles.USER)); + var claims = Enumerable.Empty() + .Append(new Claim(Claims.Subject, userId)) + .Append(new Claim(Claims.Role, NexusRoles.USER)) + .Concat(userClaims) + .Concat(tokenClaimsRead) + .Concat(tokenClaimsWrite); var identity = new ClaimsIdentity( - claims, + claims, Scheme.Name, nameType: Claims.Name, roleType: Claims.Role); @@ -84,6 +100,6 @@ protected override Task HandleAuthenticateAsync() result = AuthenticateResult.Success(ticket); } - return Task.FromResult(result); + return result; } } \ No newline at end of file diff --git a/src/Nexus/Services/CatalogManager.cs b/src/Nexus/Services/CatalogManager.cs index e26d9d5d..34441fe4 100644 --- a/src/Nexus/Services/CatalogManager.cs +++ b/src/Nexus/Services/CatalogManager.cs @@ -300,12 +300,12 @@ private CatalogPrototype[] EnsureNoHierarchy( { var owner = catalogPrototype.Owner; var ownerCanWrite = owner is null - || AuthorizationUtilities.IsCatalogWritable(catalogPrototype.Registration.Path, catalogPrototype.Metadata, owner); + || AuthUtilities.IsCatalogWritable(catalogPrototype.Registration.Path, catalogPrototype.Metadata, owner); var otherPrototype = catalogPrototypesToKeep[referenceIndex]; var otherOwner = otherPrototype.Owner; var otherOwnerCanWrite = otherOwner is null - || AuthorizationUtilities.IsCatalogWritable(otherPrototype.Registration.Path, catalogPrototype.Metadata, otherOwner); + || AuthUtilities.IsCatalogWritable(otherPrototype.Registration.Path, catalogPrototype.Metadata, otherOwner); if (!otherOwnerCanWrite && ownerCanWrite) { diff --git a/src/Nexus/Services/DataService.cs b/src/Nexus/Services/DataService.cs index e51d60b2..ee7b2a5b 100644 --- a/src/Nexus/Services/DataService.cs +++ b/src/Nexus/Services/DataService.cs @@ -102,7 +102,7 @@ public async Task ReadAsStreamAsync( var catalogContainer = catalogItemRequest.Container; // security check - if (!AuthorizationUtilities.IsCatalogReadable(catalogContainer.Id, catalogContainer.Metadata, catalogContainer.Owner, _user)) + 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 diff --git a/src/Nexus/Services/TokenService.cs b/src/Nexus/Services/TokenService.cs index b43fa4a5..c564620a 100644 --- a/src/Nexus/Services/TokenService.cs +++ b/src/Nexus/Services/TokenService.cs @@ -16,19 +16,15 @@ Task CreateAsync( IReadOnlyList claims); bool TryGet( - string tokenValue, - [NotNullWhen(true)] out string? userId, + string userId, + string secret, [NotNullWhen(true)] out InternalPersonalAccessToken? token); - Task DeleteAsync( - string userId, - Guid tokenId); + Task DeleteAsync(string userId, Guid tokenId); - Task DeleteAsync( - string tokenValue); + Task DeleteAsync(string userId, string secret); - Task> GetAllAsync( - string userId); + Task> GetAllAsync(string userId); } internal class TokenService : ITokenService @@ -74,12 +70,20 @@ public Task CreateAsync( (key, _) => token ); - var tokenValue = $"{secret}_{userId}"; - - return tokenValue; + return secret; }, saveChanges: true); } + public bool TryGet( + string userId, + string secret, + [NotNullWhen(true)] out InternalPersonalAccessToken? token) + { + var tokenMap = GetTokenMap(userId); + + return tokenMap.TryGetValue(secret, out token); + } + public Task DeleteAsync(string userId, Guid tokenId) { return InteractWithTokenMapAsync(userId, tokenMap => @@ -92,12 +96,8 @@ public Task DeleteAsync(string userId, Guid tokenId) }, saveChanges: true); } - public Task DeleteAsync(string tokenValue) + public Task DeleteAsync(string userId, string secret) { - var splittedTokenValue = tokenValue.Split('_', count: 2); - var secret = splittedTokenValue[0]; - var userId = splittedTokenValue[1]; - return InteractWithTokenMapAsync(userId, tokenMap => { tokenMap.TryRemove(secret, out _); @@ -114,19 +114,6 @@ public Task> GetAllAsyn saveChanges: false); } - public bool TryGet( - string tokenValue, - [NotNullWhen(true)] out string? userId, - [NotNullWhen(true)] out InternalPersonalAccessToken? token) - { - var splittedTokenValue = tokenValue.Split('_', count: 2); - var secret = splittedTokenValue[0]; - userId = splittedTokenValue[1]; - var tokenMap = GetTokenMap(userId); - - return tokenMap.TryGetValue(secret, out token); - } - private ConcurrentDictionary GetTokenMap( string userId) { diff --git a/src/Nexus/Utilities/AuthUtilities.cs b/src/Nexus/Utilities/AuthUtilities.cs new file mode 100644 index 00000000..f388e769 --- /dev/null +++ b/src/Nexus/Utilities/AuthUtilities.cs @@ -0,0 +1,170 @@ +using Nexus.Core; +using Nexus.Sources; +using System.Security.Claims; +using System.Text.RegularExpressions; +using static OpenIddict.Abstractions.OpenIddictConstants; + +namespace Nexus.Utilities +{ + internal static class AuthUtilities + { + public static string ComponentsToTokenValue(string secret, string userId) + { + return $"{secret}_{userId}"; + } + + public static (string userId, string secret) TokenValueToComponents(string tokenValue) + { + var parts = tokenValue.Split('_', count: 2); + + return (parts[1], parts[0]); + } + + public static bool IsCatalogReadable( + string catalogId, + CatalogMetadata catalogMetadata, + ClaimsPrincipal? owner, + ClaimsPrincipal user) + { + return InternalIsCatalogAccessible( + catalogId, + catalogMetadata, + owner, + user, + singleClaimValue: NexusClaims.CAN_READ_CATALOG, + groupClaimValue: NexusClaims.CAN_READ_CATALOG, + checkImplicitAccess: true + ); + } + + public static bool IsCatalogWritable( + string catalogId, + CatalogMetadata catalogMetadata, + ClaimsPrincipal user) + { + return InternalIsCatalogAccessible( + catalogId, + catalogMetadata, + owner: default, + user, + singleClaimValue: NexusClaims.CAN_READ_CATALOG, + groupClaimValue: NexusClaims.CAN_WRITE_CATALOG, + checkImplicitAccess: false + ); + } + + private static bool InternalIsCatalogAccessible( + string catalogId, + CatalogMetadata catalogMetadata, + ClaimsPrincipal? owner, + ClaimsPrincipal user, + string singleClaimValue, + string groupClaimValue, + bool checkImplicitAccess) + { + foreach (var identity in user.Identities) + { + if (identity is null || !identity.IsAuthenticated) + continue; + + if (catalogId == CatalogContainer.RootCatalogId) + return true; + + var implicitAccess = + catalogId == Sample.LocalCatalogId || + catalogId == Sample.RemoteCatalogId; + + if (checkImplicitAccess && implicitAccess) + return true; + + var result = false; + + /* 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 == singleClaimValue && + 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(singleClaimValue), + NexusClaims.ToPatUserClaimType(groupClaimValue)); + } + } + + /* cookie */ + else + { + var isAdmin = identity.HasClaim( + Claims.Role, + NexusRoles.ADMINISTRATOR); + + if (isAdmin) + return true; + + /* ensure that user can read that catalog */ + result = CanUserAccessCatalog( + catalogId, + catalogMetadata, + owner, + identity, + singleClaimValue, + groupClaimValue); + } + + /* leave loop when access is granted */ + if (result) + return true; + } + + return false; + } + + private static bool CanUserAccessCatalog( + string catalogId, + CatalogMetadata catalogMetadata, + ClaimsPrincipal? owner, + ClaimsIdentity identity, + string singleClaimValue, + string groupClaimValue + ) + { + var isOwner = + owner is not null && + owner?.FindFirstValue(Claims.Subject) == identity.FindFirst(Claims.Subject)?.Value; + + var canReadCatalog = identity.HasClaim( + claim => + claim.Type == singleClaimValue && + Regex.IsMatch(catalogId, claim.Value) + ); + + var canReadCatalogGroup = catalogMetadata.GroupMemberships is not null && identity.HasClaim( + claim => + claim.Type == groupClaimValue && + catalogMetadata.GroupMemberships.Any(group => Regex.IsMatch(group, claim.Value)) + ); + + return isOwner || canReadCatalog || canReadCatalogGroup; + } + } +} diff --git a/src/Nexus/Utilities/AuthorizationUtilities.cs b/src/Nexus/Utilities/AuthorizationUtilities.cs deleted file mode 100644 index 2dc43349..00000000 --- a/src/Nexus/Utilities/AuthorizationUtilities.cs +++ /dev/null @@ -1,70 +0,0 @@ -using Nexus.Core; -using Nexus.Sources; -using System.Security.Claims; -using System.Text.RegularExpressions; -using static OpenIddict.Abstractions.OpenIddictConstants; - -namespace Nexus.Utilities -{ - internal static class AuthorizationUtilities - { - public static bool IsCatalogReadable( - string catalogId, - CatalogMetadata catalogMetadata, - ClaimsPrincipal? owner, - ClaimsPrincipal user) - { - var identity = user.Identity; - - if (identity is not null && identity.IsAuthenticated) - { - if (catalogId == CatalogContainer.RootCatalogId) - return true; - - var isAdmin = user.IsInRole(NexusRoles.ADMINISTRATOR); - var isOwner = owner is not null && owner?.FindFirstValue(Claims.Subject) == user.FindFirstValue(Claims.Subject); - - var canReadCatalog = user.HasClaim( - claim => claim.Type == NexusClaims.CAN_READ_CATALOG && - Regex.IsMatch(catalogId, claim.Value)); - - var canReadCatalogGroup = catalogMetadata.GroupMemberships is not null && user.HasClaim( - claim => claim.Type == NexusClaims.CAN_READ_CATALOG_GROUP && - catalogMetadata.GroupMemberships.Any(group => Regex.IsMatch(group, claim.Value))); - - var implicitAccess = - catalogId == Sample.LocalCatalogId || - catalogId == Sample.RemoteCatalogId; - - return isAdmin || isOwner || canReadCatalog || canReadCatalogGroup || implicitAccess; - } - - return false; - } - - public static bool IsCatalogWritable( - string catalogId, - CatalogMetadata catalogMetadata, - ClaimsPrincipal user) - { - var identity = user.Identity; - - if (identity is not null && identity.IsAuthenticated) - { - var isAdmin = user.IsInRole(NexusRoles.ADMINISTRATOR); - - var canWriteCatalog = user.HasClaim( - claim => claim.Type == NexusClaims.CAN_WRITE_CATALOG && - Regex.IsMatch(catalogId, claim.Value)); - - var canWriteCatalogGroup = catalogMetadata.GroupMemberships is not null && user.HasClaim( - claim => claim.Type == NexusClaims.CAN_WRITE_CATALOG_GROUP && - catalogMetadata.GroupMemberships.Any(group => Regex.IsMatch(group, claim.Value))); - - return isAdmin || canWriteCatalog || canWriteCatalogGroup; - } - - return false; - } - } -} diff --git a/tests/Nexus.Tests/Other/PackageControllerTests.cs b/tests/Nexus.Tests/Other/PackageControllerTests.cs index fb4978d1..5a563438 100644 --- a/tests/Nexus.Tests/Other/PackageControllerTests.cs +++ b/tests/Nexus.Tests/Other/PackageControllerTests.cs @@ -18,7 +18,7 @@ public class PackageControllerTests // However, this token - in combination with the test user's account // privileges - allows only read-only access to a test project, so there // is no real risk. - private static byte[] _token = + 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, diff --git a/tests/Nexus.Tests/Other/UtilitiesTests.cs b/tests/Nexus.Tests/Other/UtilitiesTests.cs index b8582ede..4dae3b98 100644 --- a/tests/Nexus.Tests/Other/UtilitiesTests.cs +++ b/tests/Nexus.Tests/Other/UtilitiesTests.cs @@ -22,7 +22,7 @@ public class UtilitiesTests [InlineData("Basic", false, new string[0], new string[] { "A2" }, false)] [InlineData(null, true, new string[0], new string[0], false)] public void CanDetermineCatalogAccessibility( - string authenticationType, + string? authenticationType, bool isAdmin, string[] canReadCatalog, string[] canReadCatalogGroup, @@ -46,7 +46,7 @@ public void CanDetermineCatalogAccessibility( roleType: Claims.Role)); // Act - var actual = AuthorizationUtilities.IsCatalogReadable(catalogId, catalogMetadata, default!, principal); + var actual = AuthUtilities.IsCatalogReadable(catalogId, catalogMetadata, default!, principal); // Assert Assert.Equal(expected, actual); @@ -62,7 +62,7 @@ public void CanDetermineCatalogAccessibility( [InlineData("Basic", false, new string[0], false)] [InlineData(null, true, new string[0], false)] public void CanDetermineCatalogEditability( - string authenticationType, + string? authenticationType, bool isAdmin, string[] canWriteCatalog, bool expected) @@ -84,7 +84,7 @@ public void CanDetermineCatalogEditability( roleType: Claims.Role)); // Act - var actual = AuthorizationUtilities.IsCatalogWritable(catalogId, catalogMetadata, principal); + var actual = AuthUtilities.IsCatalogWritable(catalogId, catalogMetadata, principal); // Assert Assert.Equal(expected, actual); diff --git a/tests/Nexus.Tests/Services/DataControllerServiceTests.cs b/tests/Nexus.Tests/Services/DataControllerServiceTests.cs index cd483667..3acce127 100644 --- a/tests/Nexus.Tests/Services/DataControllerServiceTests.cs +++ b/tests/Nexus.Tests/Services/DataControllerServiceTests.cs @@ -53,7 +53,7 @@ public async Task CanCreateAndInitializeDataSourceController() var encodedRequestConfiguration = Convert.ToBase64String(JsonSerializer.SerializeToUtf8Bytes(requestConfiguration)); var httpContext = new DefaultHttpContext(); - httpContext.Request.Headers.Add(DataControllerService.NexusConfigurationHeaderKey, encodedRequestConfiguration); + httpContext.Request.Headers.Append(DataControllerService.NexusConfigurationHeaderKey, encodedRequestConfiguration); var httpContextAccessor = Mock.Of(); diff --git a/tests/Nexus.Tests/Services/TokenServiceTests.cs b/tests/Nexus.Tests/Services/TokenServiceTests.cs index 60f52634..a41aad67 100644 --- a/tests/Nexus.Tests/Services/TokenServiceTests.cs +++ b/tests/Nexus.Tests/Services/TokenServiceTests.cs @@ -61,6 +61,37 @@ await tokenService.CreateAsync( }); } + [Fact] + public void CanTryGetToken() + { + // Arrange + var expectedDescription = "The description"; + + var tokenMap = new Dictionary() + { + ["abc"] = new InternalPersonalAccessToken( + default, + Description: string.Empty, + Expires: default, + Claims: new List() + ), + ["def"] = new InternalPersonalAccessToken( + default, + Description: "The description", + Expires: default, + Claims: new List() + ) + }; + + var tokenService = GetTokenService(default!, tokenMap); + + // Act + var actual = tokenService.TryGet("starlord", "def", out var actualToken); + + Assert.True(actual); + Assert.Equal(expectedDescription, actualToken!.Description); + } + [Fact] public async Task CanDeleteTokenByValue() { @@ -88,7 +119,7 @@ public async Task CanDeleteTokenByValue() var tokenService = GetTokenService(filePath, tokenMap); // Act - await tokenService.DeleteAsync("abc_userid"); + await tokenService.DeleteAsync("starlord", "abc"); // Assert tokenMap.Remove("abc"); @@ -185,7 +216,7 @@ private ITokenService GetTokenService(string filePath, Dictionary databaseService.WriteTokenMap(It.IsAny())) - .Returns(File.OpenWrite(filePath)); + .Returns(() => File.OpenWrite(filePath)); var tokenService = new TokenService(databaseService); From 6ec808c6c667f9aef9245758d5b29c624b07f666 Mon Sep 17 00:00:00 2001 From: Apollo3zehn Date: Thu, 7 Mar 2024 20:27:32 +0100 Subject: [PATCH 13/19] Fix tests --- notes/auth.md | 14 +++-- src/Nexus/Utilities/AuthUtilities.cs | 30 ++++----- tests/Nexus.Tests/Other/UtilitiesTests.cs | 74 ++++++++++++++++------- 3 files changed, 76 insertions(+), 42 deletions(-) diff --git a/notes/auth.md b/notes/auth.md index 8f3035d4..c9225c2a 100644 --- a/notes/auth.md +++ b/notes/auth.md @@ -1,7 +1,13 @@ # Note -The text below does not apply anymore to Nexus because we have switched from refresh tokens + access tokens to personal access tokens that expire only optionally and are not cryptographically signed but checked against the database instead. The negible problem of higher database load is acceptible to get the benefit of not having to manage refresh tokens which are prone to being revoked as soon as the user uses it in more than a single place. +The text below does not fully apply anymore to Nexus because we have switched from refresh tokens + access tokens to personal access tokens that expire only optionally and are not cryptographically signed but checked against the database instead. The negible problem of higher database load is acceptible to get the benefit of not having to manage refresh tokens which are prone to being revoked as soon as the user uses it in more than a single place. -The new personal access tokens approach allows fine-grained access control to catalogs and makes many parts of the code much simpler. +The new personal access tokens approach allows fine-grained access control to catalogs and makes many parts of the code much simpler. Current status is: +- User can manage personal access tokens in the web interface and specify read or read/write access to specific catalogs. +- The token the user gets is a string which consists of a combination of the token secret (a long random base64 encoded number) and the user id. +- Tokens are stored on disk in the folder configured by the `PathsOptions.Users` option in a files named `tokens.json`. They loaded lazily into memory on first demand and kept there for future requests. +- When the token is part of the Authorization header (`Authorization: Bearer `) it is being handles by the `PersonalAccessTokenAuthenticationHandler` which creates a `ClaimsPrincipal` if the token is valid. +- The claims that are associated with the token can be anything but right now only the claims `CanReadCatalog` and `CanWriteCatalog` are being considered. To avoid a token to be more powerful than the user itself, the user claims are also being checked (see `AuthUtilities.cs`) on each request. +- The lifetime of the tokens can be choosen by the users or left untouched to produce tokens with unlimited lifetime. # Authentication and Authorization @@ -55,7 +61,7 @@ In order to detect a compromised token, it is recommended to implement token rot ## Implementation details -The backend of Nexus is a confidential client upon user request, it will perform the authorization code flow to obtain an ID token to authenticate and sign-in the user. +The backend of Nexus is a confidential client and upon user request, it will perform the authorization code flow to obtain an ID token to authenticate and sign-in the user. Nexus supports multiple OpenID Connect providers. See [Configuration] on how to add configuration values. @@ -71,7 +77,7 @@ The problem now is that although the access token contains the subject claim, it Another problem is that Nexus cannot add these user-specific claims to the access token, which means that the user database must be consulted for every single request, resulting in a high disk load. -Also, a such client would be public which means it is possible to copy the `client_id` and use them in other clients, which might be problematic when there is limited traffic allowed . +Also, a such client would be public which means it is possible to copy the `client_id` and use them in other clients, which might be problematic when there is limited traffic allowed. The last problem with refresh tokens is that _"for public clients [they] MUST be sender-constrained or use refresh token rotation [...]"_ [[OAuth 2.0 Security Best Current Practice](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics-19#section-2.2.2), [RFC 6749](https://datatracker.ietf.org/doc/html/rfc6749#section-4.13)]. diff --git a/src/Nexus/Utilities/AuthUtilities.cs b/src/Nexus/Utilities/AuthUtilities.cs index f388e769..ed3acbe2 100644 --- a/src/Nexus/Utilities/AuthUtilities.cs +++ b/src/Nexus/Utilities/AuthUtilities.cs @@ -31,8 +31,8 @@ public static bool IsCatalogReadable( catalogMetadata, owner, user, - singleClaimValue: NexusClaims.CAN_READ_CATALOG, - groupClaimValue: NexusClaims.CAN_READ_CATALOG, + singleClaimType: NexusClaims.CAN_READ_CATALOG, + groupClaimType: NexusClaims.CAN_READ_CATALOG_GROUP, checkImplicitAccess: true ); } @@ -47,8 +47,8 @@ public static bool IsCatalogWritable( catalogMetadata, owner: default, user, - singleClaimValue: NexusClaims.CAN_READ_CATALOG, - groupClaimValue: NexusClaims.CAN_WRITE_CATALOG, + singleClaimType: NexusClaims.CAN_WRITE_CATALOG, + groupClaimType: NexusClaims.CAN_WRITE_CATALOG_GROUP, checkImplicitAccess: false ); } @@ -58,8 +58,8 @@ private static bool InternalIsCatalogAccessible( CatalogMetadata catalogMetadata, ClaimsPrincipal? owner, ClaimsPrincipal user, - string singleClaimValue, - string groupClaimValue, + string singleClaimType, + string groupClaimType, bool checkImplicitAccess) { foreach (var identity in user.Identities) @@ -92,7 +92,7 @@ private static bool InternalIsCatalogAccessible( /* The token alone can access the catalog ... */ var canAccessCatalog = identity.HasClaim( claim => - claim.Type == singleClaimValue && + claim.Type == singleClaimType && Regex.IsMatch(catalogId, claim.Value) ); @@ -106,8 +106,8 @@ private static bool InternalIsCatalogAccessible( catalogMetadata, owner, identity, - NexusClaims.ToPatUserClaimType(singleClaimValue), - NexusClaims.ToPatUserClaimType(groupClaimValue)); + NexusClaims.ToPatUserClaimType(singleClaimType), + NexusClaims.ToPatUserClaimType(groupClaimType)); } } @@ -127,8 +127,8 @@ private static bool InternalIsCatalogAccessible( catalogMetadata, owner, identity, - singleClaimValue, - groupClaimValue); + singleClaimType, + groupClaimType); } /* leave loop when access is granted */ @@ -144,8 +144,8 @@ private static bool CanUserAccessCatalog( CatalogMetadata catalogMetadata, ClaimsPrincipal? owner, ClaimsIdentity identity, - string singleClaimValue, - string groupClaimValue + string singleClaimType, + string groupClaimType ) { var isOwner = @@ -154,13 +154,13 @@ owner is not null && var canReadCatalog = identity.HasClaim( claim => - claim.Type == singleClaimValue && + claim.Type == singleClaimType && Regex.IsMatch(catalogId, claim.Value) ); var canReadCatalogGroup = catalogMetadata.GroupMemberships is not null && identity.HasClaim( claim => - claim.Type == groupClaimValue && + claim.Type == groupClaimType && catalogMetadata.GroupMemberships.Any(group => Regex.IsMatch(group, claim.Value)) ); diff --git a/tests/Nexus.Tests/Other/UtilitiesTests.cs b/tests/Nexus.Tests/Other/UtilitiesTests.cs index 4dae3b98..f25b2347 100644 --- a/tests/Nexus.Tests/Other/UtilitiesTests.cs +++ b/tests/Nexus.Tests/Other/UtilitiesTests.cs @@ -12,35 +12,47 @@ public class UtilitiesTests { [Theory] - [InlineData("Basic", true, new string[0], new string[0], true)] - [InlineData("Basic", false, new string[] { "/D/E/F", "/A/B/C", "/G/H/I" }, new string[0], true)] - [InlineData("Basic", false, new string[] { "^/A/B/.*" }, new string[0], true)] - [InlineData("Basic", false, new string[0], new string[] { "A" }, true)] - - [InlineData("Basic", false, new string[0], new string[0], false)] - [InlineData("Basic", false, new string[] { "/D/E/F", "/A/B/C2", "/G/H/I" }, new string[0], false)] - [InlineData("Basic", false, new string[0], new string[] { "A2" }, false)] - [InlineData(null, true, new string[0], new string[0], false)] - public void CanDetermineCatalogAccessibility( + [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: new[] { "A" }, default); + var catalogMetadata = new CatalogMetadata(default, GroupMemberships: ["A"], default); var adminClaim = isAdmin - ? new Claim[] { new Claim(Claims.Role, NexusRoles.ADMINISTRATOR) } + ? [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(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)); @@ -52,33 +64,49 @@ public void CanDetermineCatalogAccessibility( Assert.Equal(expected, actual); } - // TODO Extend test for 'CAN_WRITE_CATALOG' [Theory] - [InlineData("Basic", true, new string[0], true)] - [InlineData("Basic", false, new string[] { "/D/E/F", "/A/B/C", "/G/H/I" }, true)] - [InlineData("Basic", false, new string[] { "^/A/B/.*" }, true)] + [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("Basic", false, new string[0], false)] - [InlineData(null, true, new string[0], false)] - public void CanDetermineCatalogEditability( + [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: new[] { "A" }, default); + var catalogMetadata = new CatalogMetadata(default, GroupMemberships: ["A"], default); var adminClaim = isAdmin - ? new Claim[] { new Claim(Claims.Role, NexusRoles.ADMINISTRATOR) } + ? [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(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)); From f402fb15a0368275a944e2c5590b5de12278196e Mon Sep 17 00:00:00 2001 From: Apollo3zehn <20972129+Apollo3zehn@users.noreply.github.com> Date: Thu, 7 Mar 2024 22:06:33 +0100 Subject: [PATCH 14/19] [Add] Improve .editorconfig and run dotnet format (#48) * Formatting * Use file scoped namespaces * Remove regions * Prefer var * Add .venv to .gitignore --------- Co-authored-by: Apollo3zehn --- .editorconfig | 21 +- .gitignore | 1 + .vscode/settings.json | 3 +- pyrightconfig.json | 2 +- .../Charts/AvailabilityChart.razor.cs | 211 +- src/Nexus.UI/Charts/Chart.razor.cs | 1539 +++++++-------- src/Nexus.UI/Charts/ChartTypes.cs | 123 +- .../Components/Leftbar_ChartSettings.razor | 7 +- .../Components/UserSettingsView.razor | 2 +- src/Nexus.UI/Core/AppState.cs | 20 - src/Nexus.UI/Pages/ChartTest.razor.cs | 91 +- .../NexusAuthenticationStateProvider.cs | 69 +- src/Nexus.UI/Services/TypeFaceService.cs | 63 +- src/Nexus.UI/ViewModels/SettingsViewModel.cs | 12 +- src/Nexus/API/ArtifactsController.cs | 75 +- src/Nexus/API/CatalogsController.cs | 931 +++++---- src/Nexus/API/DataController.cs | 109 +- src/Nexus/API/JobsController.cs | 515 +++-- src/Nexus/API/PackageReferencesController.cs | 171 +- src/Nexus/API/SourcesController.cs | 285 ++- src/Nexus/API/SystemController.cs | 133 +- src/Nexus/API/UsersController.cs | 685 ++++--- src/Nexus/API/WritersController.cs | 61 +- src/Nexus/Core/AppState.cs | 41 +- 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 | 107 +- 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 +- ...ersonalAccessTokenAuthenticationHandler.cs | 6 +- src/Nexus/Core/StreamInputFormatter.cs | 19 +- src/Nexus/Core/UserDbContext.cs | 35 +- .../DataSource/DataSourceController.cs | 1743 ++++++++--------- .../DataSourceControllerExtensions.cs | 149 +- .../DataSource/DataSourceControllerTypes.cs | 17 +- .../DataSource/DataSourceDoubleStream.cs | 173 +- .../DataWriter/DataWriterController.cs | 415 ++-- .../DataWriter/DataWriterControllerTypes.cs | 11 +- src/Nexus/Extensions/Sources/Sample.cs | 407 ++-- src/Nexus/Extensions/Writers/Csv.cs | 525 +++-- src/Nexus/Extensions/Writers/CsvTypes.cs | 51 +- .../PackageManagement/PackageController.cs | 1315 ++++++------- .../PackageManagement/PackageLoadContext.cs | 55 +- src/Nexus/Program.cs | 2 +- src/Nexus/Services/AppStateManager.cs | 461 +++-- src/Nexus/Services/CacheService.cs | 315 ++- src/Nexus/Services/CatalogManager.cs | 501 +++-- src/Nexus/Services/DataControllerService.cs | 199 +- src/Nexus/Services/DataService.cs | 627 +++--- src/Nexus/Services/DatabaseService.cs | 559 +++--- src/Nexus/Services/DbService.cs | 187 +- src/Nexus/Services/ExtensionHive.cs | 325 ++- src/Nexus/Services/JobService.cs | 171 +- src/Nexus/Services/MemoryTracker.cs | 211 +- src/Nexus/Services/ProcessingService.cs | 16 +- src/Nexus/Services/TokenService.cs | 12 +- src/Nexus/Utilities/AuthUtilities.cs | 277 ++- src/Nexus/Utilities/BufferUtilities.cs | 61 +- src/Nexus/Utilities/GenericsUtilities.cs | 89 +- src/Nexus/Utilities/JsonSerializerHelper.cs | 39 +- src/Nexus/Utilities/MemoryManager.cs | 35 +- 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 | 225 +-- .../DataModel/Resource.cs | 221 +-- .../DataModel/ResourceBuilder.cs | 145 +- .../DataModel/ResourceCatalog.cs | 313 ++- .../DataModel/ResourceCatalogBuilder.cs | 195 +- .../DataSource/DataSourceTypes.cs | 155 +- .../Extensibility/DataSource/IDataSource.cs | 145 +- .../DataSource/SimpleDataSource.cs | 117 +- .../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 +- .../Other/PackageControllerTests.cs | 109 +- tests/Nexus.Tests/Other/UtilitiesTests.cs | 535 +++-- .../Services/MemoryTrackerTests.cs | 2 +- .../Nexus.Tests/Services/TokenServiceTests.cs | 6 +- tests/Nexus.Tests/myappsettings.json | 2 +- tests/TestExtensionProject/TestDataSource.cs | 53 +- tests/TestExtensionProject/TestDataWriter.cs | 37 +- .../dotnet-client-tests/ClientTests.cs | 120 +- .../DataModelExtensionsTests.cs | 149 +- .../DataModelFixture.cs | 279 ++- .../DataModelTests.cs | 541 +++-- 111 files changed, 12157 insertions(+), 12512 deletions(-) diff --git a/.editorconfig b/.editorconfig index a4a437ba..2fbcf6b6 100755 --- a/.editorconfig +++ b/.editorconfig @@ -1,14 +1,29 @@ -# 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 error/info/warn/ + +[*] +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 +# 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/.gitignore b/.gitignore index 4339ec80..4bbc1fa1 100755 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .vs/ .venv/ + artifacts/ BenchmarkDotNet.Artifacts 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/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..0e9d358d 100644 --- a/src/Nexus.UI/Charts/Chart.razor.cs +++ b/src/Nexus.UI/Charts/Chart.razor.cs @@ -5,1015 +5,998 @@ using SkiaSharp; using SkiaSharp.Views.Blazor; -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!; +namespace Nexus.UI.Charts; - /* zoom */ - private bool _isDragging; - private readonly DotNetObjectReference _dotNetHelper; +public partial class Chart : IDisposable +{ + private SKGLView _skiaView = default!; + private readonly string _chartId = Guid.NewGuid().ToString(); + private Dictionary _axesMap = default!; - private SKRect _oldZoomBox; - private SKRect _zoomBox; - private Position _zoomStart; - private Position _zoomEnd; + /* zoom */ + private bool _isDragging; + private readonly DotNetObjectReference _dotNetHelper; - private DateTime _zoomedBegin; - private DateTime _zoomedEnd; + private SKRect _oldZoomBox; + private SKRect _zoomBox; + private Position _zoomStart; + private Position _zoomEnd; - /* Common */ - private const float TICK_SIZE = 10; + private DateTime _zoomedBegin; + private DateTime _zoomedEnd; - /* 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; + /* Common */ + private const float TICK_SIZE = 10; - private const float AXIS_MARGIN_RIGHT = 5; - private const float HALF_LINE_HEIGHT = 3.5f; + /* 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 readonly int[] _factors = new int[] { 2, 5, 10, 20, 50 }; + private const float AXIS_MARGIN_RIGHT = 5; + private const float HALF_LINE_HEIGHT = 3.5f; - /* 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; + private readonly int[] _factors = [2, 5, 10, 20, 50]; - /* Others */ - private bool _beginAtZero; - private readonly SKColor[] _colors; + /* 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; - #endregion + /* Others */ + private bool _beginAtZero; + private readonly SKColor[] _colors; - #region Constructors + public Chart() + { + _dotNetHelper = DotNetObjectReference.Create(this); - public Chart() + _timeAxisConfigs = new[] { - _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) - }; - } - - #endregion - - #region Properties + /* 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) + }; + } - [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 - { - if (value != _beginAtZero) - { - _beginAtZero = value; - - Task.Run(() => - { - _axesMap = LineSeriesData.Series - .GroupBy(lineSeries => lineSeries.Unit) - .ToDictionary(group => GetAxisInfo(group.Key, group), group => group.ToArray()); - - _skiaView.Invalidate(); - }); - } - } + return _beginAtZero; } - - #endregion - - #region Callbacks - - protected override void OnInitialized() + set { - /* line series color */ - for (int i = 0; i < LineSeriesData.Series.Count; i++) + if (value != _beginAtZero) { - var color = _colors[i % _colors.Length]; - LineSeriesData.Series[i].Color = color; - } + _beginAtZero = value; - /* axes info */ - _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()); - /* zoom */ - ResetZoom(); + _skiaView.Invalidate(); + }); + } } + } - private void OnMouseDown(MouseEventArgs e) + protected override void OnInitialized() + { + /* line series color */ + for (int i = 0; i < LineSeriesData.Series.Count; i++) { - var position = JSRuntime.Invoke("nexus.chart.toRelative", _chartId, e.ClientX, e.ClientY); - _zoomStart = position; - _zoomEnd = position; - - JSRuntime.InvokeVoid("nexus.util.addMouseUpEvent", _dotNetHelper); - - _isDragging = true; + var color = _colors[i % _colors.Length]; + LineSeriesData.Series[i].Color = color; } - [JSInvokable] - public void OnMouseUp() - { - _isDragging = false; - - JSRuntime.InvokeVoid("nexus.chart.resize", _chartId, "selection", 0, 1, 0, 0); + /* axes info */ + _axesMap = LineSeriesData.Series + .GroupBy(lineSeries => lineSeries.Unit) + .ToDictionary(group => GetAxisInfo(group.Key, group), group => group.ToArray()); - var zoomBox = CreateZoomBox(_zoomStart, _zoomEnd); + /* zoom */ + ResetZoom(); + } - if (zoomBox.Width > 0 && - zoomBox.Height > 0) - { - ApplyZoom(zoomBox); - _skiaView.Invalidate(); - } - } + private void OnMouseDown(MouseEventArgs e) + { + var position = JSRuntime.Invoke("nexus.chart.toRelative", _chartId, e.ClientX, e.ClientY); + _zoomStart = position; + _zoomEnd = position; - private void OnMouseMove(MouseEventArgs e) - { - var relativePosition = JSRuntime.Invoke("nexus.chart.toRelative", _chartId, e.ClientX, e.ClientY); - DrawAuxiliary(relativePosition); - } + JSRuntime.InvokeVoid("nexus.util.addMouseUpEvent", _dotNetHelper); - private void OnMouseLeave(MouseEventArgs e) - { - JSRuntime.InvokeVoid("nexus.chart.hide", _chartId, "crosshairs-x"); - JSRuntime.InvokeVoid("nexus.chart.hide", _chartId, "crosshairs-y"); + _isDragging = true; + } - foreach (var series in LineSeriesData.Series) - { - JSRuntime.InvokeVoid("nexus.chart.hide", _chartId, $"pointer_{series.Id}"); - JSRuntime.InvokeVoid("nexus.chart.setTextContent", _chartId, $"value_{series.Id}", "--"); - } - } + [JSInvokable] + public void OnMouseUp() + { + _isDragging = false; - private void OnDoubleClick(MouseEventArgs e) - { - ResetZoom(); + JSRuntime.InvokeVoid("nexus.chart.resize", _chartId, "selection", 0, 1, 0, 0); - var relativePosition = JSRuntime.Invoke("nexus.chart.toRelative", _chartId, e.ClientX, e.ClientY); - DrawAuxiliary(relativePosition); + var zoomBox = CreateZoomBox(_zoomStart, _zoomEnd); + if (zoomBox.Width > 0 && + zoomBox.Height > 0) + { + ApplyZoom(zoomBox); _skiaView.Invalidate(); } + } - private void OnWheel(WheelEventArgs e) - { - const float FACTOR = 0.25f; - - var relativePosition = JSRuntime.Invoke("nexus.chart.toRelative", _chartId, e.ClientX, e.ClientY); + private void OnMouseMove(MouseEventArgs e) + { + var relativePosition = JSRuntime.Invoke("nexus.chart.toRelative", _chartId, e.ClientX, e.ClientY); + DrawAuxiliary(relativePosition); + } - var zoomBox = new SKRect - { - Left = relativePosition.X * (e.DeltaY < 0 - ? +FACTOR // +0.25 - : -FACTOR), // -0.25 + private void OnMouseLeave(MouseEventArgs e) + { + JSRuntime.InvokeVoid("nexus.chart.hide", _chartId, "crosshairs-x"); + JSRuntime.InvokeVoid("nexus.chart.hide", _chartId, "crosshairs-y"); - Top = relativePosition.Y * (e.DeltaY < 0 - ? +FACTOR // +0.25 - : -FACTOR), // -0.25 + foreach (var series in LineSeriesData.Series) + { + JSRuntime.InvokeVoid("nexus.chart.hide", _chartId, $"pointer_{series.Id}"); + JSRuntime.InvokeVoid("nexus.chart.setTextContent", _chartId, $"value_{series.Id}", "--"); + } + } - Right = relativePosition.X + (1 - relativePosition.X) * (e.DeltaY < 0 - ? (1 - FACTOR) // +0.75 - : (1 + FACTOR)), // +1.25 + private void OnDoubleClick(MouseEventArgs e) + { + ResetZoom(); - 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); + DrawAuxiliary(relativePosition); + _skiaView.Invalidate(); + } - ApplyZoom(zoomBox); - DrawAuxiliary(relativePosition); + private void OnWheel(WheelEventArgs e) + { + const float FACTOR = 0.25f; - _skiaView.Invalidate(); - } + var relativePosition = JSRuntime.Invoke("nexus.chart.toRelative", _chartId, e.ClientX, e.ClientY); - private void ToggleSeriesEnabled(LineSeries series) + var zoomBox = new SKRect { - series.Show = !series.Show; - _skiaView.Invalidate(); - } + Left = relativePosition.X * (e.DeltaY < 0 + ? +FACTOR // +0.25 + : -FACTOR), // -0.25 - #endregion + Top = relativePosition.Y * (e.DeltaY < 0 + ? +FACTOR // +0.25 + : -FACTOR), // -0.25 - #region Draw + Right = relativePosition.X + (1 - relativePosition.X) * (e.DeltaY < 0 + ? (1 - FACTOR) // +0.75 + : (1 + FACTOR)), // +1.25 - private void PaintSurface(SKPaintGLSurfaceEventArgs e) - { - /* sizes */ - var canvas = e.Surface.Canvas; - var surfaceSize = e.BackendRenderTarget.Size; + Bottom = relativePosition.Y + (1 - relativePosition.Y) * (e.DeltaY < 0 + ? (1 - FACTOR) // +0.75 + : (1 + FACTOR)) // +1.25 + }; - var yMin = Y_PADDING_TOP; - var yMax = surfaceSize.Height - Y_PADDING_Bottom; - var xMin = Y_PADDING_LEFT; - var xMax = surfaceSize.Width; - /* y-axis */ - xMin = DrawYAxes(canvas, xMin, yMin, yMax, _axesMap); - yMin += Y_UNIT_OFFSET; + ApplyZoom(zoomBox); + DrawAuxiliary(relativePosition); - /* time-axis */ - DrawTimeAxis(canvas, xMin, yMin, xMax, yMax, _zoomedBegin, _zoomedEnd); + _skiaView.Invalidate(); + } - /* series */ - var dataBox = new SKRect(xMin, yMin, xMax, yMax); + private void ToggleSeriesEnabled(LineSeries series) + { + series.Show = !series.Show; + _skiaView.Invalidate(); + } - using (var canvasRestore = new SKAutoCanvasRestore(canvas)) - { - canvas.ClipRect(dataBox); + #region Draw - /* for each axis */ - foreach (var axesEntry in _axesMap) - { - var axisInfo = axesEntry.Key; - var lineSeries = axesEntry.Value; + private void PaintSurface(SKPaintGLSurfaceEventArgs e) + { + /* sizes */ + var canvas = e.Surface.Canvas; + var surfaceSize = e.BackendRenderTarget.Size; - /* for each dataset */ - foreach (var series in lineSeries) - { - var zoomInfo = GetZoomInfo(dataBox, _zoomBox, series.Data); - DrawSeries(canvas, zoomInfo, series, axisInfo); - } - } - } + var yMin = Y_PADDING_TOP; + var yMax = surfaceSize.Height - Y_PADDING_Bottom; + var xMin = Y_PADDING_LEFT; + var xMax = surfaceSize.Width; - /* overlay */ - JSRuntime.InvokeVoid( - "nexus.chart.resize", - _chartId, - "overlay", - dataBox.Left / surfaceSize.Width, - dataBox.Top / surfaceSize.Height, - dataBox.Right / surfaceSize.Width, - dataBox.Bottom / surfaceSize.Height); - } + /* y-axis */ + xMin = DrawYAxes(canvas, xMin, yMin, yMax, _axesMap); + yMin += Y_UNIT_OFFSET; - private void DrawAuxiliary(Position relativePosition) - { - // datetime - var zoomedTimeRange = _zoomedEnd - _zoomedBegin; - var currentTimeBegin = _zoomedBegin + zoomedTimeRange * relativePosition.X; - var currentTimeBeginString = currentTimeBegin.ToString(_timeAxisConfig.CursorLabelFormat); + /* time-axis */ + DrawTimeAxis(canvas, xMin, yMin, xMax, yMax, _zoomedBegin, _zoomedEnd); - JSRuntime.InvokeVoid("nexus.chart.setTextContent", _chartId, $"value_datetime", currentTimeBeginString); + /* series */ + var dataBox = new SKRect(xMin, yMin, xMax, yMax); - // crosshairs - JSRuntime.InvokeVoid("nexus.chart.translate", _chartId, "crosshairs-x", 0, relativePosition.Y); - JSRuntime.InvokeVoid("nexus.chart.translate", _chartId, "crosshairs-y", relativePosition.X, 0); + using (var canvasRestore = new SKAutoCanvasRestore(canvas)) + { + canvas.ClipRect(dataBox); - // 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]; - return axisInfo; + if (!double.IsNaN(value)) + { + if (value < min) + min = value; + + 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; + } + + private void ResetZoom() + { + /* zoom box */ + _oldZoomBox = new SKRect(0, 0, 1, 1); + _zoomBox = new SKRect(0, 0, 1, 1); - /* time range */ - _zoomedBegin = LineSeriesData.Begin; - _zoomedEnd = LineSeriesData.End; + /* time range */ + _zoomedBegin = LineSeriesData.Begin; + _zoomedEnd = LineSeriesData.End; - /* data range */ - foreach (var axesEntry in _axesMap) - { - var axisInfo = axesEntry.Key; + /* data range */ + foreach (var axesEntry in _axesMap) + { + var axisInfo = axesEntry.Key; - axisInfo.Min = axisInfo.OriginalMin; - axisInfo.Max = axisInfo.OriginalMax; - } + 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) + 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); + + /* 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; + } - /* range and position of first significant digit */ - var range = max - min; - var significant = (int)Math.Round(Math.Log10(range), MidpointRounding.AwayFromZero); + 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); - /* get limits */ + /* get limits */ + minLimit = (float)RoundDown(min, decimalPlaces: -significant); + maxLimit = (float)RoundUp(max, decimalPlaces: -significant); + + /* 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; + var factor = _factors[i]; - for (int i = 0; i < _factors.Length; 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 + #endregion - #region Helpers + #region Helpers - private static string ToEngineering(double value) - { - if (value == 0) - return "0"; + private static string ToEngineering(double value) + { + if (value == 0) + return "0"; - if (Math.Abs(value) < 1000) - return value.ToString("G4"); + if (Math.Abs(value) < 1000) + return value.ToString("G4"); - var exponent = (int)Math.Floor(Math.Log10(Math.Abs(value))); + var exponent = (int)Math.Floor(Math.Log10(Math.Abs(value))); - var pattern = (exponent % 3) switch - { - +1 => "##.##e0", - -2 => "##.##e0", - +2 => "###.#e0", - -1 => "###.#e0", - _ => "#.###e0" - }; - - return value.ToString(pattern); - } - - 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/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.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/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.UI/ViewModels/SettingsViewModel.cs b/src/Nexus.UI/ViewModels/SettingsViewModel.cs index 7b5edf0b..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 { @@ -283,7 +275,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/ArtifactsController.cs b/src/Nexus/API/ArtifactsController.cs index 9954180a..e04aea59 100644 --- a/src/Nexus/API/ArtifactsController.cs +++ b/src/Nexus/API/ArtifactsController.cs @@ -2,58 +2,45 @@ 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} - - #region Fields + // GET /api/artifacts/{artifactId} - public IDatabaseService _databaseService; + public IDatabaseService _databaseService; - #endregion - - #region Constructors + public ArtifactsController( + IDatabaseService databaseService) + { + _databaseService = databaseService; + } - public ArtifactsController( - IDatabaseService databaseService) + /// + /// Gets the specified artifact. + /// + /// The artifact identifier. + [HttpGet("{artifactId}")] + public ActionResult + Download( + string artifactId) + { + if (_databaseService.TryReadArtifact(artifactId, out var artifactStream)) { - _databaseService = databaseService; + Response.Headers.ContentLength = artifactStream.Length; + return File(artifactStream, "application/octet-stream"); // do not set filname here, otherwise will not work! } - #endregion - - #region Methods - - /// - /// Gets the specified artifact. - /// - /// The artifact identifier. - [HttpGet("{artifactId}")] - public ActionResult - Download( - string artifactId) + else { - 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}."); - } + return NotFound($"Could not find artifact {artifactId}."); } - - #endregion } } diff --git a/src/Nexus/API/CatalogsController.cs b/src/Nexus/API/CatalogsController.cs index b4f91a09..8e50f36c 100644 --- a/src/Nexus/API/CatalogsController.cs +++ b/src/Nexus/API/CatalogsController.cs @@ -13,571 +13,558 @@ 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 { + // 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 + + private readonly AppState _appState; + private readonly IDatabaseService _databaseService; + private readonly IDataControllerService _dataControllerService; + + public CatalogsController( + AppState appState, + IDatabaseService databaseService, + IDataControllerService dataControllerService) + { + _appState = appState; + _databaseService = databaseService; + _dataControllerService = dataControllerService; + } + /// - /// Provides access to catalogs. + /// Searches for the given resource paths and returns the corresponding catalog items. /// - [Authorize] - [ApiController] - [ApiVersion("1.0")] - [Route("api/v{version:apiVersion}/[controller]")] - internal class CatalogsController : ControllerBase + /// The list of resource paths. + /// A token to cancel the current operation. + [HttpPost("search-items")] + public async Task>> + SearchCatalogItemsAsync( + [FromBody] string[] resourcePaths, + CancellationToken cancellationToken) { - // 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; - } - - #endregion + var root = _appState.CatalogState.Root; - #region Methods + // translate resource paths to catalog item requests + (string ResourcePath, CatalogItemRequest Request)[] resourcePathAndRequests; - /// - /// 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) + try { - var root = _appState.CatalogState.Root; - - // translate resource paths to catalog item requests - (string ResourcePath, CatalogItemRequest Request)[] resourcePathAndRequests; - - try - { - 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}."); - - return (resourcePath, catalogItemRequest); - })); - } - catch (ValidationException ex) + resourcePathAndRequests = await Task.WhenAll(resourcePaths.Distinct().Select(async resourcePath => { - return UnprocessableEntity(ex.Message); - } + var catalogItemRequest = await root + .TryFindAsync(resourcePath, cancellationToken) + ?? throw new ValidationException($"Could not find resource path {resourcePath}."); - // authorize - try - { - foreach (var group in resourcePathAndRequests.GroupBy(current => current.Request.Container.Id)) - { - var catalogContainer = group.First().Request.Container; + return (resourcePath, catalogItemRequest); + })); + } + catch (ValidationException ex) + { + return UnprocessableEntity(ex.Message); + } - 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) + // authorize + try + { + foreach (var group in resourcePathAndRequests.GroupBy(current => current.Request.Container.Id)) { - return StatusCode(StatusCodes.Status403Forbidden, ex.Message); - } - - var response = resourcePathAndRequests - .ToDictionary(item => item.ResourcePath, item => item.Request.Item); + var catalogContainer = group.First().Request.Container; - return response; + if (!AuthUtilities.IsCatalogReadable(catalogContainer.Id, catalogContainer.Metadata, catalogContainer.Owner, User)) + throw new UnauthorizedAccessException($"The current user is not permitted to access catalog {catalogContainer.Id}."); + } } - - /// - /// 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 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); - /// - /// 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) + 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; + } + + /// + /// 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); - if (ensureWritable && !AuthUtilities.IsCatalogWritable(catalogContainer.Id, catalogContainer.Metadata, User)) - return StatusCode(StatusCodes.Status403Forbidden, $"The current user is not permitted to modify the catalog {catalogId}."); + var response = ProtectCatalogAsync(catalogId, ensureReadable: true, ensureWritable: false, async catalogContainer => + { + 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); + + return response; + } - return await action.Invoke(catalogContainer); + 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); - #endregion + 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}."); + + return await action.Invoke(catalogContainer); + } + else + { + return NotFound(catalogId); + } } } diff --git a/src/Nexus/API/DataController.cs b/src/Nexus/API/DataController.cs index b9dd9447..23b4969e 100644 --- a/src/Nexus/API/DataController.cs +++ b/src/Nexus/API/DataController.cs @@ -5,74 +5,65 @@ 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 + private readonly IDataService _dataService; - private readonly IDataService _dataService; + public DataController( + IDataService dataService) + { + _dataService = dataService; + } - #endregion + /// + /// Gets the requested data. + /// + /// The path to the resource data to stream. + /// Start date/time. + /// End date/time. + /// A cancellation token. + /// - #region Constructors + [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(); - public DataController( - IDataService dataService) + try { - _dataService = dataService; - } + var stream = await _dataService.ReadAsStreamAsync(resourcePath, begin, end, cancellationToken); - #endregion - - /// - /// 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) + Response.Headers.ContentLength = stream.Length; + return File(stream, "application/octet-stream", "data.bin"); + } + catch (ValidationException ex) { - resourcePath = WebUtility.UrlDecode(resourcePath); - begin = begin.ToUniversalTime(); - end = end.ToUniversalTime(); - - 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); - } + 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..99e09471 100644 --- a/src/Nexus/API/JobsController.cs +++ b/src/Nexus/API/JobsController.cs @@ -6,60 +6,77 @@ 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 + + private readonly AppStateManager _appStateManager; + private readonly ILogger _logger; + private readonly IServiceProvider _serviceProvider; + private readonly Serilog.IDiagnosticContext _diagnosticContext; + private readonly IJobService _jobService; + + public JobsController( + AppStateManager appStateManager, + IJobService jobService, + IServiceProvider serviceProvider, + Serilog.IDiagnosticContext diagnosticContext, + ILogger logger) + { + _appStateManager = appStateManager; + _jobService = jobService; + _serviceProvider = serviceProvider; + _diagnosticContext = diagnosticContext; + _logger = logger; + } + + #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(); + + return result; + } - /// - /// Gets a list of jobs. - /// - /// - [HttpGet] - public ActionResult> GetJobs() + /// + /// 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 +84,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..022d801e 100644 --- a/src/Nexus/API/PackageReferencesController.cs +++ b/src/Nexus/API/PackageReferencesController.cs @@ -3,109 +3,96 @@ 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 + + private readonly AppState _appState; + private readonly AppStateManager _appStateManager; + private readonly IExtensionHive _extensionHive; + + public PackageReferencesController( + AppState appState, + AppStateManager appStateManager, + IExtensionHive extensionHive) + { + _appState = appState; + _appStateManager = appStateManager; + _extensionHive = extensionHive; + } + /// - /// 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); + return _appState.Project.PackageReferences + .ToDictionary( + entry => entry.Key, + entry => new PackageReference(entry.Value.Provider, entry.Value.Configuration)); + } - await _appStateManager.PutPackageReferenceAsync(internalPackageReference); + /// + /// 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); - return internalPackageReference.Id; - } + await _appStateManager.PutPackageReferenceAsync(internalPackageReference); - /// - /// Deletes a package reference. - /// - /// The ID of the package reference. - [HttpDelete("{packageReferenceId}")] - public Task DeleteAsync( - Guid packageReferenceId) - { - return _appStateManager.DeletePackageReferenceAsync(packageReferenceId); - } + return internalPackageReference.Id; + } - /// - /// 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; + /// + /// Deletes a package reference. + /// + /// The ID of the package reference. + [HttpDelete("{packageReferenceId}")] + public Task DeleteAsync( + Guid packageReferenceId) + { + return _appStateManager.DeletePackageReferenceAsync(packageReferenceId); + } - if (!project.PackageReferences.TryGetValue(packageReferenceId, out var packageReference)) - return NotFound($"Unable to find package reference with ID {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; - var result = await _extensionHive - .GetVersionsAsync(packageReference, cancellationToken); + if (!project.PackageReferences.TryGetValue(packageReferenceId, out var packageReference)) + return NotFound($"Unable to find package reference with ID {packageReferenceId}."); - return result; - } + var result = await _extensionHive + .GetVersionsAsync(packageReference, cancellationToken); - #endregion + return result; } } \ No newline at end of file diff --git a/src/Nexus/API/SourcesController.cs b/src/Nexus/API/SourcesController.cs index c4595b81..9f3954ec 100644 --- a/src/Nexus/API/SourcesController.cs +++ b/src/Nexus/API/SourcesController.cs @@ -8,181 +8,172 @@ 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 { + // GET /api/sources/descriptions + // GET /api/sources/registrations + // POST /api/sources/registrations + // DELETE /api/sources/registrations/{registrationId} + + private readonly AppState _appState; + private readonly AppStateManager _appStateManager; + private readonly IExtensionHive _extensionHive; + + public SourcesController( + AppState appState, + AppStateManager appStateManager, + IExtensionHive extensionHive) + { + _appState = appState; + _appStateManager = appStateManager; + _extensionHive = extensionHive; + } + /// - /// Provides access to extensions. + /// Gets the list of source descriptions. /// - [Authorize] - [ApiController] - [ApiVersion("1.0")] - [Route("api/v{version:apiVersion}/[controller]")] - internal class SourcesController : ControllerBase + [HttpGet("descriptions")] + public List GetDescriptions() { - // GET /api/sources/descriptions - // GET /api/sources/registrations - // 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 + var result = GetExtensionDescriptions(_extensionHive.GetExtensions()); + return result; + } - public SourcesController( - AppState appState, - AppStateManager appStateManager, - IExtensionHive extensionHive) + /// + /// 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)) { - _appState = appState; - _appStateManager = appStateManager; - _extensionHive = extensionHive; - } + 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))); - #endregion + else + return Ok(new Dictionary()); + } - /// - /// Gets the list of source descriptions. - /// - [HttpGet("descriptions")] - public List GetDescriptions() + else { - var result = GetExtensionDescriptions(_extensionHive.GetExtensions()); - return result; + return response; } + } - /// - /// 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) + /// + /// 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)) { - 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()); - } - - else - { - return 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); } - /// - /// 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); - } - - else - { - return response; - } + 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) + /// + /// 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)) { - if (TryAuthenticate(userId, out var actualUserId, out var response)) - { - await _appStateManager.DeleteDataSourceRegistrationAsync(actualUserId, registrationId); - return Ok(); - } - - else - { - return 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..064f37a7 100644 --- a/src/Nexus/API/SystemController.cs +++ b/src/Nexus/API/SystemController.cs @@ -5,86 +5,73 @@ 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 - - // [privileged] - // PUT /api/system/configuration - - #region Fields - - private readonly AppState _appState; - private readonly AppStateManager _appStateManager; - private readonly GeneralOptions _generalOptions; +namespace Nexus.Controllers; - #endregion - - #region Constructors - - public SystemController( - AppState appState, - AppStateManager appStateManager, - IOptions generalOptions) - { - _generalOptions = generalOptions.Value; - _appState = appState; - _appStateManager = appStateManager; - } +/// +/// 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 - #endregion + // [privileged] + // PUT /api/system/configuration - #region Methods + private readonly AppState _appState; + private readonly AppStateManager _appStateManager; + private readonly GeneralOptions _generalOptions; - /// - /// Gets the default file type. - /// - [HttpGet("file-type")] - public string? GetDefaultFileType() - { - return _generalOptions.DefaultFileType; - } + public SystemController( + AppState appState, + AppStateManager appStateManager, + IOptions generalOptions) + { + _generalOptions = generalOptions.Value; + _appState = appState; + _appStateManager = appStateManager; + } - /// - /// 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); } } diff --git a/src/Nexus/API/UsersController.cs b/src/Nexus/API/UsersController.cs index e54834d2..ca37cbfb 100644 --- a/src/Nexus/API/UsersController.cs +++ b/src/Nexus/API/UsersController.cs @@ -13,426 +13,417 @@ 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 + + // [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} + + // GET /api/users/{userId}/claims + // POST /api/users/{userId}/claims + // DELETE /api/users/claims/{claimId} + + // GET /api/users/{userId}/tokens + + private readonly IDBService _dbService; + private readonly ITokenService _tokenService; + private readonly SecurityOptions _securityOptions; + private readonly ILogger _logger; + + public UsersController( + IDBService dBService, + ITokenService tokenService, + IOptions securityOptions, + ILogger logger) { - // [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} - - // [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}/tokens - - #region Fields - - private readonly IDBService _dbService; - private readonly ITokenService _tokenService; - private readonly SecurityOptions _securityOptions; - private readonly ILogger _logger; - - #endregion + _dbService = dBService; + _tokenService = tokenService; + _securityOptions = securityOptions.Value; + _logger = logger; + } - #region Constructors + #region Anonymous - public UsersController( - IDBService dBService, - ITokenService tokenService, - IOptions securityOptions, - ILogger logger) - { - _dbService = dBService; - _tokenService = tokenService; - _securityOptions = securityOptions.Value; - _logger = logger; - } - - #endregion + /// + /// 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 }; - #region Anonymous + return providers + .Select(provider => new AuthenticationSchemeDescription(provider.Scheme, provider.DisplayName)) + .ToList(); + } - /// - /// Returns a list of available authentication schemes. - /// - [AllowAnonymous] - [HttpGet("authentication-schemes")] - public List GetAuthenticationSchemes() + /// + /// 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 providers = _securityOptions.OidcProviders.Any() - ? _securityOptions.OidcProviders - : new List() { NexusAuthExtensions.DefaultProvider }; - - return providers - .Select(provider => new AuthenticationSchemeDescription(provider.Scheme, provider.DisplayName)) - .ToList(); - } + RedirectUri = 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() - { - 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() + { + 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); + } - /// - /// Gets the current user. - /// - [HttpGet("me")] - public async Task> GetMeAsync() + /// + /// 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..3c2aeb12 100644 --- a/src/Nexus/API/WritersController.cs +++ b/src/Nexus/API/WritersController.cs @@ -2,46 +2,33 @@ 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 - - #region Fields - - private readonly AppState _appState; - - #endregion - - #region Constructors + // GET /api/writers/descriptions - public WritersController( - AppState appState) - { - _appState = appState; - } + private readonly AppState _appState; - #endregion - - #region Methods - - /// - /// Gets the list of writer descriptions. - /// - [HttpGet("descriptions")] - public List GetDescriptions() - { - return _appState.DataWriterDescriptions; - } + public WritersController( + AppState appState) + { + _appState = appState; + } - #endregion + /// + /// Gets the list of writer descriptions. + /// + [HttpGet("descriptions")] + public List GetDescriptions() + { + return _appState.DataWriterDescriptions; } } diff --git a/src/Nexus/Core/AppState.cs b/src/Nexus/Core/AppState.cs index 1f3a1349..53cbeed6 100644 --- a/src/Nexus/Core/AppState.cs +++ b/src/Nexus/Core/AppState.cs @@ -2,38 +2,29 @@ using System.Collections.Concurrent; using System.Reflection; -namespace Nexus.Core +namespace Nexus.Core; + +internal class AppState { - internal class AppState + public AppState() { - #region Constructors - - public AppState() - { - var entryAssembly = Assembly.GetEntryAssembly()!; - var version = entryAssembly.GetName().Version!; - - Version = version.ToString(); - } - - #endregion + var entryAssembly = Assembly.GetEntryAssembly()!; + var version = entryAssembly.GetName().Version!; - #region Properties - General - - public ConcurrentDictionary> ResourceCache { get; } - = new ConcurrentDictionary>(); + Version = version.ToString(); + } - public string Version { get; } + public ConcurrentDictionary> ResourceCache { get; } + = new ConcurrentDictionary>(); - public Task? ReloadPackagesTask { get; set; } + public string Version { get; } - // these properties will be set during host startup - public NexusProject Project { get; set; } = default!; + public Task? ReloadPackagesTask { get; set; } - public CatalogState CatalogState { get; set; } = default!; + // these properties will be set during host startup + public NexusProject Project { get; set; } = default!; - public List DataWriterDescriptions { get; set; } = default!; + public CatalogState CatalogState { get; set; } = default!; - #endregion - } + public List DataWriterDescriptions { get; set; } = default!; } 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 dff2c501..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); - - internal record struct Interval( - DateTime Begin, - DateTime End); +namespace Nexus.Core; - internal record ReadUnit( - CatalogItemRequest CatalogItemRequest, - PipeWriter DataWriter); +internal record InternalPersonalAccessToken( + Guid Id, + string Description, + DateTime Expires, + IReadOnlyList Claims); - internal record CatalogItemRequest( - CatalogItem Item, - CatalogItem? BaseItem, - CatalogContainer Container); +internal record struct Interval( + DateTime Begin, + DateTime End); - internal record NexusProject( - IReadOnlyDictionary? SystemConfiguration, - IReadOnlyDictionary PackageReferences, - IReadOnlyDictionary UserConfigurations); +internal record ReadUnit( + CatalogItemRequest CatalogItemRequest, + PipeWriter DataWriter); - internal record UserConfiguration( - IReadOnlyDictionary DataSourceRegistrations); +internal record CatalogItemRequest( + CatalogItem Item, + CatalogItem? BaseItem, + CatalogContainer Container); - internal record CatalogState( - CatalogContainer Root, - CatalogCache Cache); +internal record NexusProject( + IReadOnlyDictionary? SystemConfiguration, + IReadOnlyDictionary PackageReferences, + IReadOnlyDictionary UserConfigurations); - internal record LazyCatalogInfo( - DateTime Begin, - DateTime End, - ResourceCatalog Catalog); +internal record UserConfiguration( + IReadOnlyDictionary DataSourceRegistrations); - internal record ExportContext( - TimeSpan SamplePeriod, - IEnumerable CatalogItemRequests, - ReadDataHandler ReadDataHandler, - ExportParameters ExportParameters); +internal record CatalogState( + CatalogContainer Root, + CatalogCache Cache); - internal record JobControl( - DateTime Start, - Job Job, - CancellationTokenSource CancellationTokenSource) - { - public event EventHandler? ProgressUpdated; - public event EventHandler? Completed; +internal record LazyCatalogInfo( + DateTime Begin, + DateTime End, + ResourceCatalog Catalog); + +internal record ExportContext( + TimeSpan SamplePeriod, + IEnumerable CatalogItemRequests, + ReadDataHandler ReadDataHandler, + ExportParameters ExportParameters); + +internal record JobControl( + DateTime Start, + Job Job, + CancellationTokenSource CancellationTokenSource) +{ + public event EventHandler? ProgressUpdated; + public event EventHandler? Completed; - public double Progress { get; private set; } + public double Progress { get; private set; } - public Task Task { get; set; } = default!; + public Task Task { get; set; } = default!; - public void OnProgressUpdated(double e) - { - Progress = e; - ProgressUpdated?.Invoke(this, e); - } + public void OnProgressUpdated(double e) + { + Progress = e; + ProgressUpdated?.Invoke(this, e); + } - public void OnCompleted() - { - Completed?.Invoke(this, EventArgs.Empty); - } + 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/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/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 0baf1926..b5c86123 100644 --- a/src/Nexus/Extensibility/DataSource/DataSourceController.cs +++ b/src/Nexus/Extensibility/DataSource/DataSourceController.cs @@ -10,1103 +10,1078 @@ using Nexus.Services; using Nexus.Utilities; -namespace Nexus.Extensibility +namespace Nexus.Extensibility; + +internal interface IDataSourceController : IDisposable { - 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); - } + 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 class DataSourceController : IDataSourceController +{ + private readonly IProcessingService _processingService; + private readonly ICacheService _cacheService; + private readonly DataOptions _dataOptions; + private ConcurrentDictionary _catalogCache = default!; + + public DataSourceController( + IDataSource dataSource, + InternalDataSourceRegistration registration, + IReadOnlyDictionary? systemConfiguration, + IReadOnlyDictionary? requestConfiguration, + IProcessingService processingService, + ICacheService cacheService, + DataOptions dataOptions, + ILogger logger) { - #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 - - #region Properties - - private IDataSource DataSource { get; } + DataSource = dataSource; + DataSourceRegistration = registration; + SystemConfiguration = systemConfiguration; + RequestConfiguration = requestConfiguration; + Logger = logger; + + _processingService = processingService; + _cacheService = cacheService; + _dataOptions = dataOptions; + } - private InternalDataSourceRegistration DataSourceRegistration { get; } + private IDataSource DataSource { get; } - private IReadOnlyDictionary? SystemConfiguration { get; } + private InternalDataSourceRegistration DataSourceRegistration { get; } - internal IReadOnlyDictionary? RequestConfiguration { get; } + private IReadOnlyDictionary? SystemConfiguration { get; } - private ILogger Logger { get; } + internal IReadOnlyDictionary? RequestConfiguration { get; } - #endregion + private ILogger Logger { get; } - #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) + { + var isModified = false; + var newResources = new List(); - // clean up "groups" property so it contains only unique groups - if (catalog.Resources is not null) + 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]; - - var tasks = new List(capacity: count); - var currentBegin = begin; + /* GetOrAdd is not working because it requires a synchronous delegate */ + _catalogCache.TryAdd(catalogId, catalog); - 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; - - using var scope = Logger.BeginScope(new Dictionary() - { - ["ResourcePath"] = catalogItemRequest.Item.ToPath() - }); + ["ResourcePath"] = catalogItemRequest.Item.ToPath() + }); - cancellationToken.ThrowIfCancellationRequested(); + cancellationToken.ThrowIfCancellationRequested(); - var buffer = dataWriter - .GetMemory(targetByteCount)[..targetByteCount]; + var buffer = dataWriter + .GetMemory(targetByteCount)[..targetByteCount]; - var targetBuffer = new CastMemoryManager(buffer).Memory; + var targetBuffer = new CastMemoryManager(buffer).Memory; - 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)); - } - - /* 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"); - - List uncachedIntervals; + readRequestManager.Dispose(); + } + } + } - var disableCache = _dataOptions.CachePattern is not null && !Regex.IsMatch(readUnit.CatalogItemRequest.Item.Catalog.Id, _dataOptions.CachePattern); + 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; - if (disableCache) - { - uncachedIntervals = new List { new Interval(begin, end) }; - } + /* target buffer */ + var buffer = readUnit.DataWriter + .GetMemory(targetByteCount)[..targetByteCount]; - else - { - uncachedIntervals = await _cacheService.ReadAsync( - item, - begin, - targetBuffer, - cancellationToken); - } + var targetBuffer = new CastMemoryManager(buffer).Memory; - /* load and process remaining data from source */ - Logger.LogTrace("Load and process {PeriodCount} uncached periods from source", uncachedIntervals.Count); + /* read request */ + var readElementCount = ExtensibilityUtilities.CalculateElementCount(begin, end, baseSamplePeriod); - var elementSize = baseItem.Representation.ElementSize; - var sourceSamplePeriod = baseSamplePeriod; - var targetSamplePeriod = samplePeriod; + using var readRequestManager = new ReadRequestManager(baseItem, readElementCount); + var readRequest = readRequestManager.Request; - var blockSize = item.Representation.Kind == RepresentationKind.Resampled - ? (int)(sourceSamplePeriod.Ticks / targetSamplePeriod.Ticks) - : (int)(targetSamplePeriod.Ticks / sourceSamplePeriod.Ticks); + /* go */ + try + { + /* load data from cache */ + Logger.LogTrace("Load data from cache"); - foreach (var interval in uncachedIntervals) - { - var offset = interval.Begin - begin; - var length = interval.End - interval.Begin; + List uncachedIntervals; - 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); - } + var disableCache = _dataOptions.CachePattern is not null && !Regex.IsMatch(readUnit.CatalogItemRequest.Item.Catalog.Id, _dataOptions.CachePattern); - /* update cache */ - if (!disableCache) - { - await _cacheService.UpdateAsync( - item, - begin, - targetBuffer, - uncachedIntervals, - cancellationToken); - } - } - catch (OutOfMemoryException) + if (disableCache) { - throw; + uncachedIntervals = new List { new Interval(begin, end) }; } - catch (Exception ex) - { - Logger.LogError(ex, "Read aggregation data period {Begin} to {End} failed", 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(); + } + + 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); - #region Static Methods + if (!catalogItemRequestPipeWriters.Any()) + return; - public static async Task ReadAsync( - DateTime begin, - DateTime end, - TimeSpan samplePeriod, - DataReadingGroup[] readingGroups, - ReadDataHandler readDataHandler, - IMemoryTracker memoryTracker, - IProgress? progress, - ILogger logger, - CancellationToken cancellationToken) + 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; + if (baseItemSamplePeriod < samplePeriod) + throw new ValidationException("Unable to resample data if the base sample period is <= the sample period."); - // 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.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); + /* total period */ + var totalPeriod = end - begin; + logger.LogTrace("The total period is {TotalPeriod}", totalPeriod); - /* bytes per row */ + /* bytes per row */ - // 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. - - 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 { - /* periods */ - var consumedPeriod = TimeSpan.Zero; - var remainingPeriod = totalPeriod; - var currentPeriod = default(TimeSpan); + await ReadCoreAsync( + begin, + totalPeriod, + maxPeriodPerRequest, + samplePeriod, + readingGroups, + readDataHandler, + progress, + logger, + cancellationToken); + } + finally + { + allocationRegistration.Dispose(); + } + } + + 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); - /* progress */ - var currentDataSourceProgress = new ConcurrentDictionary(); + /* progress */ + var currentDataSourceProgress = new ConcurrentDictionary(); - return Task.Run(async () => + 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 (controller, catalogItemRequestPipeWriters) = readingGroup; - var readingTasks = readingGroups.Select(async 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(); + // https://stackoverflow.com/a/62768272 (currentDataSourceProgress) + currentDataSourceProgress.AddOrUpdate(controller, progressValue, (_, _) => progressValue); - await NexusUtilities.WhenAllFailFastAsync(readingTasks, cancellationToken); + var baseProgress = consumedPeriod.Ticks / (double)totalPeriod.Ticks; + var relativeProgressFactor = currentPeriod.Ticks / (double)totalPeriod.Ticks; + var relativeProgress = currentDataSourceProgress.Sum(entry => entry.Value) * relativeProgressFactor; - /* continue in time */ - consumedPeriod += currentPeriod; - remainingPeriod -= currentPeriod; - - 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); - } - - 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 - * - */ + catch (Exception ex) + { + logger.LogError(ex, "Process period {Begin} to {End} failed", currentBegin, currentEnd); + } + }).ToList(); + await NexusUtilities.WhenAllFailFastAsync(readingTasks, cancellationToken); - if (begin >= end) - throw new ValidationException("The begin datetime must be less than the end datetime."); + /* continue in time */ + consumedPeriod += currentPeriod; + remainingPeriod -= currentPeriod; - if (begin.Ticks % samplePeriod.Ticks != 0) - throw new ValidationException("The begin parameter must be a multiple of the sample period."); + progress?.Report(consumedPeriod.Ticks / (double)totalPeriod.Ticks); + } - if (end.Ticks % samplePeriod.Ticks != 0) - throw new ValidationException("The end parameter must be a multiple of the sample period."); - } + /* complete */ + foreach (var readingGroup in readingGroups) + { + foreach (var catalogItemRequestPipeWriter in readingGroup.CatalogItemRequestPipeWriters) + { + cancellationToken.ThrowIfCancellationRequested(); - #endregion + await catalogItemRequestPipeWriter.DataWriter.CompleteAsync(); + } + } + }, cancellationToken); + } - #region IDisposable + 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."); + } - 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); } } 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..d9958d74 100644 --- a/src/Nexus/Extensibility/DataSource/DataSourceDoubleStream.cs +++ b/src/Nexus/Extensibility/DataSource/DataSourceDoubleStream.cs @@ -1,124 +1,107 @@ 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; - - #endregion - - #region Constructors - - public DataSourceDoubleStream(long length, PipeReader reader) - { - _length = length; - _reader = reader; - _stream = reader.AsStream(); - } - - #endregion - - #region Properties +namespace Nexus.Extensibility; - public override bool CanRead => true; +internal class DataSourceDoubleStream : Stream +{ + private readonly CancellationTokenSource _cts = new(); + private long _position; + private readonly long _length; + private readonly PipeReader _reader; + private readonly Stream _stream; - public override bool CanSeek => false; + public DataSourceDoubleStream(long length, PipeReader reader) + { + _length = length; + _reader = reader; + _stream = reader.AsStream(); + } - 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(); - } + public void Cancel() + { + _reader.CancelPendingRead(); + } - public override int Read(Span buffer) - { - throw new NotImplementedException(); - } + public override void Flush() + { + throw new NotImplementedException(); + } - public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) - { - throw new NotImplementedException(); - } + public override int Read(byte[] buffer, int offset, int count) + { + throw new NotImplementedException(); + } - public override int EndRead(IAsyncResult asyncResult) - { - throw new NotImplementedException(); - } + public override int Read(Span buffer) + { + throw new NotImplementedException(); + } - public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } + public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) + { + 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 EndRead(IAsyncResult asyncResult) + { + throw new NotImplementedException(); + } - return readCount; - } + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } - public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) - { - 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 long Seek(long offset, SeekOrigin origin) - { - throw new NotImplementedException(); - } + return readCount; + } - public override void SetLength(long value) - { - throw new NotImplementedException(); - } + public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } - public override void Write(byte[] buffer, int offset, int count) - { - throw new NotImplementedException(); - } + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotImplementedException(); + } - protected override void Dispose(bool disposing) - { - _stream.Dispose(); - } + public override void SetLength(long value) + { + throw new NotImplementedException(); + } - #endregion + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotImplementedException(); + } + + protected override void Dispose(bool disposing) + { + _stream.Dispose(); } } diff --git a/src/Nexus/Extensibility/DataWriter/DataWriterController.cs b/src/Nexus/Extensibility/DataWriter/DataWriterController.cs index d0620559..201e8f88 100644 --- a/src/Nexus/Extensibility/DataWriter/DataWriterController.cs +++ b/src/Nexus/Extensibility/DataWriter/DataWriterController.cs @@ -4,276 +4,259 @@ 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 + 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). + private IReadOnlyDictionary? SystemConfiguration { get; } - 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; - } + private IReadOnlyDictionary? RequestConfiguration { get; } - #endregion + private IDataWriter DataWriter { get; } - #region Properties + private Uri ResourceLocator { get; } - private IReadOnlyDictionary? SystemConfiguration { get; } + private ILogger Logger { get; } - private IReadOnlyDictionary? RequestConfiguration { get; } + public async Task InitializeAsync( + ILogger logger, + CancellationToken cancellationToken) + { + var context = new DataWriterContext( + ResourceLocator: ResourceLocator, + SystemConfiguration: SystemConfiguration, + RequestConfiguration: RequestConfiguration); - private IDataWriter DataWriter { get; } + await DataWriter.SetContextAsync(context, logger, cancellationToken); + } - private Uri ResourceLocator { get; } + public async Task WriteAsync( + DateTime begin, + DateTime end, + TimeSpan samplePeriod, + TimeSpan filePeriod, + CatalogItemRequestPipeReader[] catalogItemRequestPipeReaders, + IProgress? progress, + CancellationToken cancellationToken) + { + /* validation */ + if (!catalogItemRequestPipeReaders.Any()) + return; - private ILogger Logger { get; } + foreach (var catalogItemRequestPipeReader in catalogItemRequestPipeReaders) + { + if (catalogItemRequestPipeReader.Request.Item.Representation.SamplePeriod != samplePeriod) + throw new ValidationException("All representations must be of the same sample period."); + } - #endregion + DataWriterController.ValidateParameters(begin, samplePeriod, filePeriod); - #region Methods + /* periods */ + var totalPeriod = end - begin; + Logger.LogDebug("The total period is {TotalPeriod}", totalPeriod); - public async Task InitializeAsync( - ILogger logger, - CancellationToken cancellationToken) - { - var context = new DataWriterContext( - ResourceLocator: ResourceLocator, - SystemConfiguration: SystemConfiguration, - RequestConfiguration: RequestConfiguration); + var consumedPeriod = TimeSpan.Zero; + var currentPeriod = default(TimeSpan); - await DataWriter.SetContextAsync(context, logger, cancellationToken); - } + /* progress */ + var dataWriterProgress = new Progress(); - public async Task WriteAsync( - DateTime begin, - DateTime end, - TimeSpan samplePeriod, - TimeSpan filePeriod, - CatalogItemRequestPipeReader[] catalogItemRequestPipeReaders, - IProgress? progress, - CancellationToken cancellationToken) + /* no need to remove handler because of short lifetime of IDataWriter */ + dataWriterProgress.ProgressChanged += (sender, progressValue) => { - /* validation */ - if (!catalogItemRequestPipeReaders.Any()) - return; - - foreach (var catalogItemRequestPipeReader in catalogItemRequestPipeReaders) + 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) { - if (catalogItemRequestPipeReader.Request.Item.Representation.SamplePeriod != samplePeriod) - throw new ValidationException("All representations must be of the same sample period."); + /* close */ + if (lastFileBegin != default) + await DataWriter.CloseAsync(cancellationToken); + + /* open */ + await DataWriter.OpenAsync( + fileBegin, + filePeriod, + samplePeriod, + catalogItems, + cancellationToken); } - DataWriterController.ValidateParameters(begin, samplePeriod, filePeriod); - - /* periods */ - var totalPeriod = end - begin; - Logger.LogDebug("The total period is {TotalPeriod}", totalPeriod); + lastFileBegin = fileBegin; - var consumedPeriod = TimeSpan.Zero; - var currentPeriod = default(TimeSpan); + /* loop */ + var consumedFilePeriod = TimeSpan.Zero; + var remainingPeriod = duration; - /* 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) => + while (remainingPeriod > TimeSpan.Zero) { - /* 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; + /* read */ + var readResultTasks = catalogItemRequestPipeReaders + .Select(catalogItemRequestPipeReader => catalogItemRequestPipeReader.DataReader.ReadAsync(cancellationToken)) + .ToArray(); - /* loop */ - var consumedFilePeriod = TimeSpan.Zero; - var remainingPeriod = duration; + var readResults = await NexusUtilities.WhenAll(readResultTasks); + var bufferPeriod = readResults.Min(readResult => readResult.Buffer.First.Cast().Length) * samplePeriod; - while (remainingPeriod > TimeSpan.Zero) - { - /* read */ - var readResultTasks = catalogItemRequestPipeReaders - .Select(catalogItemRequestPipeReader => catalogItemRequestPipeReader.DataReader.ReadAsync(cancellationToken)) - .ToArray(); + if (bufferPeriod == default) + throw new ValidationException("The pipe is empty."); - var readResults = await NexusUtilities.WhenAll(readResultTasks); - var bufferPeriod = readResults.Min(readResult => readResult.Buffer.First.Cast().Length) * samplePeriod; + /* write */ + currentPeriod = new TimeSpan(Math.Min(remainingPeriod.Ticks, bufferPeriod.Ticks)); + var currentLength = (int)(currentPeriod.Ticks / samplePeriod.Ticks); - if (bufferPeriod == default) - throw new ValidationException("The pipe is empty."); + var requests = catalogItemRequestPipeReaders.Zip(readResults).Select(zipped => + { + var (catalogItemRequestPipeReader, readResult) = zipped; - /* write */ - currentPeriod = new TimeSpan(Math.Min(remainingPeriod.Ticks, bufferPeriod.Ticks)); - var currentLength = (int)(currentPeriod.Ticks / samplePeriod.Ticks); + var request = catalogItemRequestPipeReader.Request; + var catalogItem = request.Item; - var requests = catalogItemRequestPipeReaders.Zip(readResults).Select(zipped => + if (request.BaseItem is not null) { - var (catalogItemRequestPipeReader, readResult) = zipped; - - var request = catalogItemRequestPipeReader.Request; - var catalogItem = request.Item; + var originalResource = request.Item.Resource; - if (request.BaseItem is not null) - { - var originalResource = request.Item.Resource; + var newResource = new ResourceBuilder(originalResource.Id) + .WithProperty(DataModelExtensions.BasePathKey, request.BaseItem.ToPath()) + .Build(); - var newResource = new ResourceBuilder(originalResource.Id) - .WithProperty(DataModelExtensions.BasePathKey, request.BaseItem.ToPath()) - .Build(); + var augmentedResource = originalResource.Merge(newResource); - var augmentedResource = originalResource.Merge(newResource); - - catalogItem = request.Item with - { - Resource = augmentedResource - }; - } - - var writeRequest = new WriteRequest( - catalogItem, - readResult.Buffer.First.Cast()[..currentLength]); - - 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."); - #region IDisposable + if (filePeriod.Ticks % samplePeriod.Ticks != 0) + throw new ValidationException("The file period parameter must be a multiple of the sample period."); + } - 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); } } \ 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..93624630 100644 --- a/src/Nexus/Extensions/Sources/Sample.cs +++ b/src/Nexus/Extensions/Sources/Sample.cs @@ -2,211 +2,255 @@ 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 + public static Guid RegistrationId = new("c2c724ab-9002-4879-9cd9-2147844bee96"); + + 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"; + + 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"; + + private DataSourceContext Context { get; set; } = default!; + + public Task SetContextAsync( + DataSourceContext context, + ILogger logger, + CancellationToken cancellationToken) { - #region Fields - - 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 - }; - - 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"; - - public const string RemoteUsername = "test"; - public const string RemotePassword = "1234"; - - #endregion - - #region Properties - - private DataSourceContext Context { get; set; } = default!; - - #endregion - - #region Methods + 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) + public async Task ReadAsync( + DateTime begin, + DateTime end, + ReadRequest[] requests, + ReadDataHandler readData, + IProgress progress, + CancellationToken cancellationToken) + { + var tasks = requests.Select(request => { - return Task.FromResult(1.0); - } + var (catalogItem, data, status) = request; - public async Task ReadAsync( - DateTime begin, - DateTime end, - ReadRequest[] requests, - ReadDataHandler readData, - IProgress progress, - CancellationToken cancellationToken) - { - var tasks = requests.Select(request => + return Task.Run(() => { - var (catalogItem, data, status) = request; + cancellationToken.ThrowIfCancellationRequested(); + + var (catalog, resource, representation, parameters) = catalogItem; - return Task.Run(() => + // 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 +265,12 @@ 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; } } diff --git a/src/Nexus/Extensions/Writers/Csv.cs b/src/Nexus/Extensions/Writers/Csv.cs index 2142d29e..516e5be9 100644 --- a/src/Nexus/Extensions/Writers/Csv.cs +++ b/src/Nexus/Extensions/Writers/Csv.cs @@ -13,350 +13,333 @@ // 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 -{ - [DataWriterDescription(DESCRIPTION)] +namespace Nexus.Writers; - [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 - { - #region "Fields" +[DataWriterDescription(DESCRIPTION)] - 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 +[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 = """ + { + "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 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 double _unixStart; - private double _excelStart; - private DateTime _lastFileBegin; - private TimeSpan _lastSamplePeriod; - private readonly Dictionary _resourceMap = new(); - - #endregion + } + """; - #region Properties + private static readonly DateTime _unixEpoch = new(1970, 01, 01); - private DataWriterContext Context { get; set; } = default!; + private static readonly NumberFormatInfo _nfi = new() + { + NumberDecimalSeparator = ".", + NumberGroupSeparator = string.Empty + }; - #endregion + 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 DataWriterContext Context { get; set; } = default!; + + public Task SetContextAsync( + DataWriterContext context, + ILogger logger, + CancellationToken cancellationToken) + { + Context = context; + return Task.CompletedTask; + } - #region "Methods" + 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(); - public Task SetContextAsync( - DataWriterContext context, - ILogger logger, - CancellationToken cancellationToken) + foreach (var catalogItemGroup in catalogItems.GroupBy(catalogItem => catalogItem.Catalog)) { - Context = context; - return Task.CompletedTask; - } + cancellationToken.ThrowIfCancellationRequested(); - 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(); + var catalog = catalogItemGroup.Key; + var physicalId = catalog.Id.TrimStart('/').Replace('/', '_'); + var root = Context.ResourceLocator.ToPath(); - foreach (var catalogItemGroup in catalogItems.GroupBy(catalogItem => catalogItem.Catalog)) - { - cancellationToken.ThrowIfCancellationRequested(); + /* metadata */ + var resourceFileNameWithoutExtension = $"{physicalId}_{samplePeriod.ToUnitString()}"; + var resourceFileName = $"{resourceFileNameWithoutExtension}.resource.json"; + var resourceFilePath = Path.Combine(root, resourceFileName); - var catalog = catalogItemGroup.Key; - var physicalId = catalog.Id.TrimStart('/').Replace('/', '_'); - var root = Context.ResourceLocator.ToPath(); + if (!_resourceMap.TryGetValue(resourceFilePath, out var resource)) + { + var rowIndexFormat = Context.RequestConfiguration?.GetStringValue("row-index-format") ?? "index"; + var constraints = new Constraints(Required: true); - /* metadata */ - var resourceFileNameWithoutExtension = $"{physicalId}_{samplePeriod.ToUnitString()}"; - var resourceFileName = $"{resourceFileNameWithoutExtension}.resource.json"; - var resourceFilePath = Path.Combine(root, resourceFileName); + 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 } + }; - if (!_resourceMap.TryGetValue(resourceFilePath, out var resource)) + var fields = new[] { timestampField }.Concat(catalogItemGroup.Select(catalogItem => { - 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; - } + 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); + /* data */ + var dataFileName = $"{physicalId}_{ToISO8601(fileBegin)}_{samplePeriod.ToUnitString()}.csv"; + var dataFilePath = Path.Combine(root, dataFileName); - if (!File.Exists(dataFilePath)) - { - var stringBuilder = new StringBuilder(); + if (!File.Exists(dataFilePath)) + { + var stringBuilder = new StringBuilder(); - using var streamWriter = new StreamWriter(File.Open(dataFilePath, FileMode.Append, FileAccess.Write), Encoding.UTF8); + using var streamWriter = new StreamWriter(File.Open(dataFilePath, FileMode.Append, FileAccess.Write), Encoding.UTF8); - /* header values */ - // TODO: use .ToString("o") instead? - stringBuilder.Append($"# date_time: {ToISO8601(fileBegin)}"); - AppendWindowsNewLine(stringBuilder); + /* header values */ + // TODO: use .ToString("o") instead? + stringBuilder.Append($"# date_time: {ToISO8601(fileBegin)}"); + AppendWindowsNewLine(stringBuilder); - stringBuilder.Append($"# sample_period: {samplePeriod.ToUnitString()}"); - AppendWindowsNewLine(stringBuilder); + stringBuilder.Append($"# sample_period: {samplePeriod.ToUnitString()}"); + AppendWindowsNewLine(stringBuilder); - stringBuilder.Append($"# catalog_id: {catalog.Id}"); - AppendWindowsNewLine(stringBuilder); + stringBuilder.Append($"# catalog_id: {catalog.Id}"); + AppendWindowsNewLine(stringBuilder); - /* field name */ - var timestampField = resource.Schema.Fields.First(); - stringBuilder.Append($"{timestampField.Name},"); + /* field name */ + var timestampField = resource.Schema.Fields.First(); + stringBuilder.Append($"{timestampField.Name},"); - foreach (var catalogItem in catalogItemGroup) - { - var fieldName = GetFieldName(catalogItem); - stringBuilder.Append($"{fieldName},"); - } + foreach (var catalogItem in catalogItemGroup) + { + var fieldName = GetFieldName(catalogItem); + stringBuilder.Append($"{fieldName},"); + } - 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); - resource.Path.Add(dataFileName); - } + 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; - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void AppendWindowsNewLine(StringBuilder stringBuilder) - { - stringBuilder.Append("\r\n"); + groupIndex++; } + } - private static string GetFieldName(CatalogItem catalogItem) - { - var unit = catalogItem.Resource.Properties? - .GetStringValue(DataModelExtensions.UnitKey); + public Task CloseAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } - var fieldName = $"{catalogItem.Resource.Id}_{catalogItem.Representation.Id}{DataModelUtilities.GetRepresentationParameterString(catalogItem.Parameters)}"; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void AppendWindowsNewLine(StringBuilder stringBuilder) + { + stringBuilder.Append("\r\n"); + } - fieldName += unit is null - ? "" - : $" ({unit})"; + private static string GetFieldName(CatalogItem catalogItem) + { + var unit = catalogItem.Resource.Properties? + .GetStringValue(DataModelExtensions.UnitKey); - return fieldName; - } + var fieldName = $"{catalogItem.Resource.Id}_{catalogItem.Representation.Id}{DataModelUtilities.GetRepresentationParameterString(catalogItem.Parameters)}"; - private static string ToISO8601(DateTime dateTime) - { - return dateTime.ToUniversalTime().ToString("yyyy-MM-ddTHH-mm-ssZ"); - } + fieldName += unit is null + ? "" + : $" ({unit})"; - #endregion + return fieldName; + } - #region IDisposable + private static string ToISO8601(DateTime dateTime) + { + return dateTime.ToUniversalTime().ToString("yyyy-MM-ddTHH-mm-ssZ"); + } - 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); } } 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 14fde8c5..cb550618 100644 --- a/src/Nexus/PackageManagement/PackageController.cs +++ b/src/Nexus/PackageManagement/PackageController.cs @@ -10,929 +10,914 @@ using ICSharpCode.SharpZipLib.Tar; using Nexus.Core; -namespace Nexus.PackageManagement +namespace Nexus.PackageManagement; + +internal partial class PackageController { - internal partial class PackageController - { - #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; + public PackageController(InternalPackageReference packageReference, ILogger logger) + { + PackageReference = packageReference; + _logger = logger; + } - #endregion + public InternalPackageReference PackageReference { get; } - #region Constructors + public async Task DiscoverAsync(CancellationToken cancellationToken) + { + _logger.LogDebug("Discover package versions using provider {Provider}", PackageReference.Provider); - public PackageController(InternalPackageReference packageReference, ILogger logger) + var result = PackageReference.Provider switch { - PackageReference = packageReference; - _logger = logger; - } - - #endregion - - #region Properties - - public InternalPackageReference PackageReference { get; } + BUILTIN_PROVIDER => ["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; + } - #endregion + public async Task LoadAsync(string restoreRoot, CancellationToken cancellationToken) + { + if (_loadContext is not null) + throw new Exception("The extension is already loaded."); - #region Methods + Assembly assembly; - public async Task DiscoverAsync(CancellationToken cancellationToken) + if (PackageReference.Provider == BUILTIN_PROVIDER) { - _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; + 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; + } - public WeakReference Unload() + internal async Task RestoreAsync(string restoreRoot, CancellationToken cancellationToken) + { + var actualRestoreRoot = Path.Combine(restoreRoot, PackageReference.Provider); + + _logger.LogDebug("Restore package to {RestoreRoot} using provider {Provider}", actualRestoreRoot, PackageReference.Provider); + var 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); - } + #region local - var assetResponse = await _httpClient - .SendAsync(assetRequest, HttpCompletionOption.ResponseHeadersRead); + private Task DiscoverLocalAsync(CancellationToken cancellationToken) + { + var rawResult = new List(); + var configuration = PackageReference.Configuration; - assetResponse.EnsureSuccessStatusCode(); + if (!configuration.TryGetValue("path", out var path)) + throw new ArgumentException("The 'path' parameter is missing in the extension reference."); - return assetResponse; - } + if (!Directory.Exists(path)) + throw new DirectoryNotFoundException($"The extension path {path} does not exist."); - // 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."); - } + 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; + _logger.LogDebug("Restore package from source {Source} to {Target}", sourcePath, targetPath); + CloneFolder(sourcePath, targetPath); + } + else + { + _logger.LogDebug("Package is already restored"); + } - if (!configuration.TryGetValue("path", out var path)) - throw new ArgumentException("The 'path' parameter is missing in the extension reference."); + return targetPath; + }, cancellationToken); + } - if (!configuration.TryGetValue("version", out var version)) - throw new ArgumentException("The 'version' parameter is missing in the extension reference."); + #endregion - var sourcePath = Path.Combine(path, version); + #region git-tag - if (!Directory.Exists(sourcePath)) - throw new DirectoryNotFoundException($"The source path {sourcePath} does not exist."); + private async Task DiscoverGitTagsAsync(CancellationToken cancellationToken) + { + const string refPrefix = "refs/tags/"; - var pathHash = new Guid(path.Hash()).ToString(); - var targetPath = Path.Combine(restoreRoot, pathHash, version); + var result = new List(); + var configuration = PackageReference.Configuration; - 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"); - } + if (!configuration.TryGetValue("repository", out var repository)) + throw new ArgumentException("The 'repository' parameter is missing in the extension reference."); - return targetPath; - }, cancellationToken); - } + var startInfo = new ProcessStartInfo + { + CreateNoWindow = true, + FileName = "git", + Arguments = $"ls-remote --tags --sort=v:refname --refs {repository}", + RedirectStandardOutput = true, + RedirectStandardError = true + }; - #endregion + using var process = Process.Start(startInfo); - #region git-tag + if (process is null) + throw new Exception("Process is null."); - private async Task DiscoverGitTagsAsync(CancellationToken cancellationToken) + while (!process.StandardOutput.EndOfStream) { - const string refPrefix = "refs/tags/"; - - var result = new List(); - var configuration = PackageReference.Configuration; - - if (!configuration.TryGetValue("repository", out var repository)) - throw new ArgumentException("The 'repository' parameter is missing in the extension reference."); + var refLine = await process.StandardOutput.ReadLineAsync(cancellationToken); - var startInfo = new ProcessStartInfo + try { - 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."); + var refString = refLine!.Split('\t')[1]; - while (!process.StandardOutput.EndOfStream) - { - var refLine = await process.StandardOutput.ReadLineAsync(cancellationToken); - - 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 - - #region gitlab-packages-generic-v4 + return targetPath; + } - // curl --header "PRIVATE-TOKEN: N1umphXDULLvgzhT7uyx" \ - // --upload-file assets.tar.gz \ - // "https:///api/v4/projects//packages/generic///assets.tar.gz" + #endregion - private async Task DiscoverGitLabPackagesGenericAsync(CancellationToken cancellationToken) - { - var result = new List(); - var configuration = PackageReference.Configuration; + #region gitlab-packages-generic-v4 - 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..ec18b297 100644 --- a/src/Nexus/PackageManagement/PackageLoadContext.cs +++ b/src/Nexus/PackageManagement/PackageLoadContext.cs @@ -1,47 +1,34 @@ using System.Reflection; using System.Runtime.Loader; -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 +namespace Nexus.PackageManagement; - #region Methods +internal class PackageLoadContext : AssemblyLoadContext +{ + private readonly AssemblyDependencyResolver _resolver; - protected override Assembly? Load(AssemblyName assemblyName) - { - var assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName); + public PackageLoadContext(string entryDllPath) : base(isCollectible: true) + { + _resolver = new AssemblyDependencyResolver(entryDllPath); + } - 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; } } 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/AppStateManager.cs b/src/Nexus/Services/AppStateManager.cs index 0ffae093..2823a0f5 100644 --- a/src/Nexus/Services/AppStateManager.cs +++ b/src/Nexus/Services/AppStateManager.cs @@ -5,310 +5,293 @@ using System.Reflection; using System.Text.Json; -namespace Nexus.Services +namespace Nexus.Services; + +internal class AppStateManager { - internal class AppStateManager + 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); + + public AppStateManager( + AppState appState, + IExtensionHive extensionHive, + ICatalogManager catalogManager, + IDatabaseService databaseService, + ILogger logger) { - #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); - - #endregion - - #region Constructors - - public AppStateManager( - AppState appState, - IExtensionHive extensionHive, - ICatalogManager catalogManager, - IDatabaseService databaseService, - ILogger logger) - { - AppState = appState; - _extensionHive = extensionHive; - _catalogManager = catalogManager; - _databaseService = databaseService; - _logger = logger; - } - - #endregion - - #region Properties - - public AppState AppState { get; } + AppState = appState; + _extensionHive = extensionHive; + _catalogManager = catalogManager; + _databaseService = databaseService; + _logger = logger; + } - #endregion + public AppState AppState { get; } - #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 + if (refreshDatabaseTask is null) { - // 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 - { - _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); } } 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..f4031714 100644 --- a/src/Nexus/Services/CatalogManager.cs +++ b/src/Nexus/Services/CatalogManager.cs @@ -7,317 +7,300 @@ 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); +} + +internal class CatalogManager : ICatalogManager +{ + record CatalogPrototype( + CatalogRegistration Registration, + InternalDataSourceRegistration DataSourceRegistration, + InternalPackageReference PackageReference, + CatalogMetadata Metadata, + ClaimsPrincipal? Owner); + + private readonly AppState _appState; + private readonly IDataControllerService _dataControllerService; + private readonly IDatabaseService _databaseService; + private readonly IServiceProvider _serviceProvider; + private readonly IExtensionHive _extensionHive; + private readonly ILogger _logger; + + public CatalogManager( + AppState appState, + IDataControllerService dataControllerService, + IDatabaseService databaseService, + IServiceProvider serviceProvider, + IExtensionHive extensionHive, + ILogger logger) { - Task GetCatalogContainersAsync( - CatalogContainer parent, - CancellationToken cancellationToken); + _appState = appState; + _dataControllerService = dataControllerService; + _databaseService = databaseService; + _serviceProvider = serviceProvider; + _extensionHive = extensionHive; + _logger = logger; } - internal class CatalogManager : ICatalogManager + public async Task GetCatalogContainersAsync( + CatalogContainer parent, + CancellationToken cancellationToken) { - #region Types - - record CatalogPrototype( - CatalogRegistration Registration, - InternalDataSourceRegistration DataSourceRegistration, - InternalPackageReference PackageReference, - CatalogMetadata Metadata, - ClaimsPrincipal? Owner); - - #endregion - - #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; - - #endregion - - #region Constructors + CatalogContainer[] catalogContainers; - public CatalogManager( - AppState appState, - IDataControllerService dataControllerService, - IDatabaseService databaseService, - IServiceProvider serviceProvider, - IExtensionHive extensionHive, - ILogger logger) + using var loggerScope = _logger.BeginScope(new Dictionary() { - _appState = appState; - _dataControllerService = dataControllerService; - _databaseService = databaseService; - _serviceProvider = serviceProvider; - _extensionHive = extensionHive; - _logger = logger; - } - - #endregion - - #region Methods + ["ParentCatalogId"] = parent.Id + }); - public async Task GetCatalogContainersAsync( - CatalogContainer parent, - CancellationToken cancellationToken) + /* special case: root */ + if (parent.Id == CatalogContainer.RootCatalogId) { - CatalogContainer[] catalogContainers; - - using var loggerScope = _logger.BeginScope(new Dictionary() + /* 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) - }; - - /* load all catalog identifiers */ - var path = CatalogContainer.RootCatalogId; - var catalogPrototypes = new List(); - - /* => for the built-in data source registrations */ + using var controller = await _dataControllerService.GetDataSourceControllerAsync(registration, cancellationToken); + var catalogRegistrations = await controller.GetCatalogRegistrationsAsync(path, cancellationToken); + var packageReference = _extensionHive.GetPackageReference(registration.Type); - // 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); + var metadata = LoadMetadata(catalogRegistration.Path); - foreach (var catalogRegistration in catalogRegistrations) - { - 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; - } - - private CatalogContainer[] ProcessCatalogPrototypes( - IEnumerable catalogPrototypes) - { - /* clean up */ - catalogPrototypes = EnsureNoHierarchy(catalogPrototypes); + return new CatalogPrototype( + catalogRegistration, + parent.DataSourceRegistration, + parent.PackageReference, + metadata, + parent.Owner); + }); - /* 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"); + + else + return new CatalogMetadata(default, default, default); + } - private CatalogPrototype[] EnsureNoHierarchy( - IEnumerable catalogPrototypes) + 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(); } } 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..e76ede98 100644 --- a/src/Nexus/Services/DataService.cs +++ b/src/Nexus/Services/DataService.cs @@ -6,396 +6,379 @@ using Nexus.Extensibility; using Nexus.Utilities; -namespace Nexus.Services +namespace Nexus.Services; + +internal interface IDataService { - 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); - } + 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 class DataService : IDataService +{ + 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; + + public DataService( + AppState appState, + ClaimsPrincipal user, + IDataControllerService dataControllerService, + IDatabaseService databaseService, + IMemoryTracker memoryTracker, + ILogger logger, + ILoggerFactory loggerFactory) { - #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(); - } + _user = user; + _appState = appState; + _dataControllerService = dataControllerService; + _databaseService = databaseService; + _memoryTracker = memoryTracker; + _logger = logger; + _loggerFactory = loggerFactory; + + ReadProgress = new Progress(); + WriteProgress = new Progress(); + } - #endregion + public Progress ReadProgress { get; } - #region Properties + public Progress WriteProgress { get; } - public Progress ReadProgress { get; } + 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 + var 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(); + licenseStream.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); - } - - return zipFileName; } + } + + private async Task CreateFilesAsync( + ExportContext exportContext, + IDataWriterController? dataWriterController, + CancellationToken cancellationToken) + { + /* reading groups */ + var catalogItemRequestPipeReaders = new List(); + var readingGroups = new List(); - private void CopyLicenseIfAvailable(string catalogId, string targetFolder) + 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); + }); - /* cancellation */ - var cts = new CancellationTokenSource(); - cancellationToken.Register(cts.Cancel); + writing = Task.WhenAll(writingTasks); + } + + /* 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 (var 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 + { + // + } } } diff --git a/src/Nexus/Services/DatabaseService.cs b/src/Nexus/Services/DatabaseService.cs index 5f686d77..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..0147fcdd 100644 --- a/src/Nexus/Services/ExtensionHive.cs +++ b/src/Nexus/Services/ExtensionHive.cs @@ -6,222 +6,209 @@ using System.Diagnostics.CodeAnalysis; using System.Reflection; -namespace Nexus.Services -{ - internal interface IExtensionHive - { - IEnumerable GetExtensions( - ) where T : IExtension; +namespace Nexus.Services; - InternalPackageReference GetPackageReference( - string fullName) where T : IExtension; - - T GetInstance( - string fullName) where T : IExtension; +internal interface IExtensionHive +{ + IEnumerable GetExtensions( + ) where T : IExtension; - Task LoadPackagesAsync( - IEnumerable packageReferences, - IProgress progress, - CancellationToken cancellationToken); + InternalPackageReference GetPackageReference( + string fullName) where T : IExtension; - Task GetVersionsAsync( - InternalPackageReference packageReference, - CancellationToken cancellationToken); - } + T GetInstance( + string fullName) where T : IExtension; - internal class ExtensionHive : IExtensionHive - { - #region Fields + Task LoadPackagesAsync( + IEnumerable packageReferences, + IProgress progress, + CancellationToken cancellationToken); - private readonly ILogger _logger; - private readonly ILoggerFactory _loggerFactory; - private readonly PathsOptions _pathsOptions; + Task GetVersionsAsync( + InternalPackageReference packageReference, + CancellationToken cancellationToken); +} - private Dictionary>? _packageControllerMap = default!; +internal class ExtensionHive : IExtensionHive +{ + private readonly ILogger _logger; + private readonly ILoggerFactory _loggerFactory; + private readonly PathsOptions _pathsOptions; - #endregion + private Dictionary>? _packageControllerMap = default!; - #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) + public async Task LoadPackagesAsync( + IEnumerable packageReferences, + IProgress progress, + CancellationToken cancellationToken) + { + // clean up + if (_packageControllerMap is not null) { - _logger = logger; - _loggerFactory = loggerFactory; - _pathsOptions = pathsOptions.Value; - } + _logger.LogDebug("Unload previously loaded packages"); - #endregion - - #region Methods - - public async Task LoadPackagesAsync( - IEnumerable packageReferences, - IProgress progress, - CancellationToken cancellationToken) - { - // clean up - if (_packageControllerMap is not null) + foreach (var (controller, _) in _packageControllerMap) { - _logger.LogDebug("Unload previously loaded packages"); - - foreach (var (controller, _) in _packageControllerMap) - { - controller.Unload(); - } - - _packageControllerMap = default; + 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; + } + + 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 (!hasParameterlessConstructor) - _logger.LogWarning("Type {TypeName} from assembly {AssemblyName} has no parameterless constructor", type.FullName, assembly.FullName); + if (isClass && isInstantiatable && (isDataSource | isDataWriter)) + { + var hasParameterlessConstructor = type.GetConstructor(Type.EmptyTypes) is not null; - return hasParameterlessConstructor; - } + if (!hasParameterlessConstructor) + _logger.LogWarning("Type {TypeName} from assembly {AssemblyName} has no parameterless constructor", type.FullName, assembly.FullName); - return false; - }) - .ToList() - .AsReadOnly(); + return hasParameterlessConstructor; + } - return foundTypes; - } + return false; + }) + .ToList() + .AsReadOnly(); - #endregion + return foundTypes; } } diff --git a/src/Nexus/Services/JobService.cs b/src/Nexus/Services/JobService.cs index d6f3ad62..c8ba9f46 100644 --- a/src/Nexus/Services/JobService.cs +++ b/src/Nexus/Services/JobService.cs @@ -3,123 +3,110 @@ using System.Diagnostics.CodeAnalysis; using Timer = System.Timers.Timer; -namespace Nexus.Services -{ - internal interface IJobService - { - JobControl AddJob( - Job job, - Progress progress, - Func> createTask); - - List GetJobs(); +namespace Nexus.Services; - bool TryGetJob( - Guid key, - [NotNullWhen(true)] out JobControl? jobControl); - } +internal interface IJobService +{ + JobControl AddJob( + Job job, + Progress progress, + Func> createTask); - internal class JobService : IJobService - { - #region Fields + List GetJobs(); - private readonly Timer _timer; + bool TryGetJob( + Guid key, + [NotNullWhen(true)] out JobControl? jobControl); +} - private readonly ConcurrentDictionary _jobs = new(); +internal class JobService : IJobService +{ + private readonly Timer _timer; - #endregion + private readonly ConcurrentDictionary _jobs = new(); - #region Constructors + public JobService() + { + _timer = new Timer() + { + AutoReset = true, + Enabled = true, + Interval = TimeSpan.FromDays(1).TotalMilliseconds + }; - public JobService() + _timer.Elapsed += (sender, e) => { - _timer = new Timer() - { - AutoReset = true, - Enabled = true, - Interval = TimeSpan.FromDays(1).TotalMilliseconds - }; + var now = DateTime.UtcNow; + var maxRuntime = TimeSpan.FromDays(3); - _timer.Elapsed += (sender, e) => + foreach (var jobControl in GetJobs()) { - var now = DateTime.UtcNow; - var maxRuntime = TimeSpan.FromDays(3); - - 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 + 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(); } } diff --git a/src/Nexus/Services/MemoryTracker.cs b/src/Nexus/Services/MemoryTracker.cs index fc9fea6d..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; - - // allow all other waiting tasks to continue - foreach (var retrySemaphore in _retrySemaphores) - { - if (retrySemaphore.CurrentCount == 0) - retrySemaphore.Release(); - } + private void SetConsumedBytesAndTriggerWaitingTasks(long difference) + { + _consumedBytes += difference; - _logger.LogTrace("{ByteCount} bytes ({MegaByteCount} MB) are currently in use", _consumedBytes, _consumedBytes / 1024 / 1024); + // allow all other waiting tasks to continue + foreach (var retrySemaphore in _retrySemaphores) + { + if (retrySemaphore.CurrentCount == 0) + retrySemaphore.Release(); } - private void MonitorFullGC() - { - _logger.LogDebug("Register for full GC notifications"); - GC.RegisterForFullGCNotification(1, 1); + _logger.LogTrace("{ByteCount} bytes ({MegaByteCount} MB) are currently in use", _consumedBytes, _consumedBytes / 1024 / 1024); + } - while (true) - { - var status = GC.WaitForFullGCApproach(); + private void MonitorFullGC() + { + _logger.LogDebug("Register for full GC notifications"); + GC.RegisterForFullGCNotification(1, 1); - if (status == GCNotificationStatus.Succeeded) - _logger.LogDebug("Full GC is approaching"); + while (true) + { + var status = GC.WaitForFullGCApproach(); - status = GC.WaitForFullGCComplete(); + if (status == GCNotificationStatus.Succeeded) + _logger.LogDebug("Full GC is approaching"); - if (status == GCNotificationStatus.Succeeded) - _logger.LogDebug("Full GC has completed"); - } + status = GC.WaitForFullGCComplete(); + + if (status == GCNotificationStatus.Succeeded) + _logger.LogDebug("Full GC has completed"); } } } diff --git a/src/Nexus/Services/ProcessingService.cs b/src/Nexus/Services/ProcessingService.cs index 912b422e..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 => { @@ -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..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); + catalogId, + catalogMetadata, + owner, + identity, + 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 e8a194a3..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(BufferUtilities.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 - { - BufferUtilities.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) - { - BufferUtilities.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 88d9193e..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), new Type[] { 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 6bcb4f70..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 76cb4e94..729de5ea 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]+)"); + var 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 (int 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..bbcf8de7 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 var 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 12abd97a..7dd6ebfa 100644 --- a/src/extensibility/dotnet-extensibility/DataModel/Representation.cs +++ b/src/extensibility/dotnet-extensibility/DataModel/Representation.cs @@ -3,154 +3,137 @@ 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 { + private static readonly Regex _snakeCaseEvaluator = new("(?<=[a-z])([A-Z])", RegexOptions.Compiled); + private static readonly HashSet _nexusDataTypeValues = new(Enum.GetValues()); + + private IReadOnlyDictionary? _parameters; + /// - /// A representation is part of a resource. + /// Initializes a new instance of the . /// - [DebuggerDisplay("{Id,nq}")] - public record Representation + /// 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) { - #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 . - /// - /// 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 - - #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 { - get - { - return _parameters; - } - - init - { - if (value is not null) - ValidateParameters(value); - - _parameters = value; - } + return _parameters; } - /// - /// The representation kind. - /// - [JsonIgnore] - #warning Remove this when https://github.com/RicoSuter/NSwag/issues/4681 is solved - internal RepresentationKind Kind { get; } + init + { + if (value is not null) + ValidateParameters(value); - /// - /// The number of bits per element. - /// - [JsonIgnore] - public int ElementSize => ((int)DataType & 0xFF) >> 3; + _parameters = value; + } + } - #endregion + /// + /// The representation kind. + /// + [JsonIgnore] +#warning Remove this when https://github.com/RicoSuter/NSwag/issues/4681 is solved + internal RepresentationKind Kind { get; } - #region "Methods" + /// + /// The number of bits per element. + /// + [JsonIgnore] + public int ElementSize => ((int)DataType & 0xFF) >> 3; - 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 } } \ 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 dce5bef3..29bf620a 100644 --- a/src/extensibility/dotnet-extensibility/DataModel/Resource.cs +++ b/src/extensibility/dotnet-extensibility/DataModel/Resource.cs @@ -3,147 +3,130 @@ 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 { + private string _id = default!; + private IReadOnlyList? _representations; + /// - /// 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; + } + + /// + /// 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]*$"); + + /// + /// 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); + + /// + /// 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); + + /// + /// Gets the identifier. + /// + public string Id + { + get { - Id = id; - Properties = properties; - Representations = representations; + return _id; } - #endregion - - #region Properties - - /// - /// 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]*$"); - - /// - /// 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); - - /// - /// 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); - - /// - /// Gets the identifier. - /// - public string Id + init { - get - { - return _id; - } - - init - { - if (!ValidIdExpression.IsMatch(value)) - throw new ArgumentException($"The resource identifier {value} is not valid."); - - _id = value; - } + if (!ValidIdExpression.IsMatch(value)) + throw new ArgumentException($"The resource identifier {value} is not valid."); + + _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? Representations + /// + /// Gets the list of representations. + /// + public IReadOnlyList? Representations + { + get { - get - { - return _representations; - } - - init - { - if (value is not null) - ValidateRepresentations(value); - - _representations = value; - } + return _representations; } - #endregion - - #region "Methods" - - 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 - }; + internal Resource Merge(Resource resource) + { + if (Id != resource.Id) + throw new ArgumentException("The resources to be merged have different identifiers."); - return merged; - } + var mergedProperties = DataModelUtilities.MergeProperties(Properties, resource.Properties); + var mergedRepresentations = DataModelUtilities.MergeRepresentations(Representations, resource.Representations); - internal Resource DeepCopy() + var merged = resource with { - return new Resource( - id: Id, - representations: Representations?.Select(representation => representation.DeepCopy()).ToList(), - properties: Properties?.ToDictionary(entry => entry.Key, entry => entry.Value.Clone())); - } + Properties = mergedProperties, + Representations = mergedRepresentations + }; - private static void ValidateRepresentations(IReadOnlyList representations) - { - var uniqueIds = representations - .Select(current => current.Id) - .Distinct(); + return merged; + } - if (uniqueIds.Count() != representations.Count) - throw new ArgumentException("There are multiple representations with the same identifier."); - } + 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())); + } + + private static void ValidateRepresentations(IReadOnlyList representations) + { + var uniqueIds = representations + .Select(current => current.Id) + .Distinct(); - #endregion + if (uniqueIds.Count() != representations.Count) + throw new ArgumentException("There are multiple representations with the same identifier."); } } \ 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..ac1fdaa4 100644 --- a/src/extensibility/dotnet-extensibility/DataModel/ResourceBuilder.cs +++ b/src/extensibility/dotnet-extensibility/DataModel/ResourceBuilder.cs @@ -1,99 +1,86 @@ 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 { + private readonly string _id; + private Dictionary? _properties; + private List? _representations; + /// - /// A resource builder simplifies building a resource. + /// Initializes a new instance of the . /// - [DebuggerDisplay("{Id,nq}")] - public record ResourceBuilder + /// The identifier of the resource to be built. + public ResourceBuilder(string id) { - #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(); + _id = id; + } - _properties[key] = JsonSerializer.SerializeToElement(value); + /// + /// 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(); - return this; - } + _properties[key] = JsonSerializer.SerializeToElement(value); - /// - /// Adds a . - /// - /// The . - /// The resource builder. - public ResourceBuilder AddRepresentation(Representation representation) - { - _representations ??= new List(); + return this; + } - _representations.Add(representation); + /// + /// Adds a . + /// + /// The . + /// The resource builder. + public ResourceBuilder AddRepresentation(Representation representation) + { + _representations ??= new List(); - return this; - } + _representations.Add(representation); - /// - /// Adds a list of . - /// - /// The list of . - /// The resource builder. - public ResourceBuilder AddRepresentations(params Representation[] representations) - { - return AddRepresentations((IEnumerable)representations); - } + return this; + } - /// - /// Adds a list of . - /// - /// The list of . - /// The resource builder. - public ResourceBuilder AddRepresentations(IEnumerable representations) - { - _representations ??= new List(); + /// + /// Adds a list of . + /// + /// The list of . + /// The resource builder. + public ResourceBuilder AddRepresentations(params Representation[] representations) + { + return AddRepresentations((IEnumerable)representations); + } - _representations.AddRange(representations); + /// + /// Adds a list of . + /// + /// The list of . + /// The resource builder. + public ResourceBuilder AddRepresentations(IEnumerable representations) + { + _representations ??= new List(); - return this; - } + _representations.AddRange(representations); - /// - /// Builds the . - /// - /// The . - public Resource Build() - { - return new Resource(_id, _properties, _representations); - } + return this; + } - #endregion + /// + /// Builds the . + /// + /// The . + public Resource Build() + { + return new Resource(_id, _properties, _representations); } } \ 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 6c75786e..941c6667 100644 --- a/src/extensibility/dotnet-extensibility/DataModel/ResourceCatalog.cs +++ b/src/extensibility/dotnet-extensibility/DataModel/ResourceCatalog.cs @@ -5,218 +5,201 @@ 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 { + private string _id = default!; + private IReadOnlyList? _resources; + /// - /// 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; - } - - #endregion - - #region Properties + Id = id; + Properties = properties; + Resources = resources; + } - /// - /// 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); + /// + /// 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); - [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); + [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); - /// - /// 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 + /// + /// 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."); - #region "Methods" + var mergedProperties = DataModelUtilities.MergeProperties(Properties, catalog.Properties); + var mergedResources = DataModelUtilities.MergeResources(Resources, catalog.Resources); - /// - /// Merges another catalog with this instance. - /// - /// The catalog to merge into this instance. - /// The merged catalog. - public ResourceCatalog Merge(ResourceCatalog catalog) + var merged = catalog with { - if (Id != catalog.Id) - throw new ArgumentException("The catalogs to be merged have different identifiers."); + Properties = mergedProperties, + Resources = mergedResources + }; - var mergedProperties = DataModelUtilities.MergeProperties(Properties, catalog.Properties); - var mergedResources = DataModelUtilities.MergeResources(Resources, catalog.Resources); - - 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 = - - (representation.Parameters is null && parameters is null) || + var parametersAreOK = - (representation.Parameters is not null && parameters is not null && - representation.Parameters.All(current => + (representation.Parameters is null && parameters is null) || - parameters.ContainsKey(current.Key) && + (representation.Parameters is not null && parameters is not null && + representation.Parameters.All(current => - (current.Value.GetStringValue("type") == "input-integer" && long.TryParse(parameters[current.Key], out var _) || - current.Value.GetStringValue("type") == "select" && true /* no validation here */))); + parameters.ContainsKey(current.Key) && - if (!parametersAreOK) - return false; + (current.Value.GetStringValue("type") == "input-integer" && long.TryParse(parameters[current.Key], out var _) || + current.Value.GetStringValue("type") == "select" && true /* no validation here */))); - catalogItem = new CatalogItem( - this with { Resources = default }, - resource with { Representations = default }, - representation, - parameters); + if (!parametersAreOK) + return false; - return true; - } + catalogItem = new CatalogItem( + this with { Resources = default }, + resource with { Representations = default }, + representation, + parameters); - 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."); } } diff --git a/src/extensibility/dotnet-extensibility/DataModel/ResourceCatalogBuilder.cs b/src/extensibility/dotnet-extensibility/DataModel/ResourceCatalogBuilder.cs index 8c36719b..0a7b588c 100644 --- a/src/extensibility/dotnet-extensibility/DataModel/ResourceCatalogBuilder.cs +++ b/src/extensibility/dotnet-extensibility/DataModel/ResourceCatalogBuilder.cs @@ -1,112 +1,99 @@ using System.Text.Json; -namespace Nexus.DataModel +namespace Nexus.DataModel; + +/// +/// A resource catalog builder simplifies building a resource catalog. +/// +public record ResourceCatalogBuilder { + private readonly string _id; + private Dictionary? _properties; + private List? _resources; + + /// + /// Initializes a new instance of the . + /// + /// The identifier of the resource catalog to be built. + public ResourceCatalogBuilder(string id) + { + _id = id; + } + + /// + /// 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; + } + /// - /// A resource catalog builder simplifies building a resource catalog. + /// Builds the . /// - public record ResourceCatalogBuilder + /// The . + public ResourceCatalog Build() { - #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 new ResourceCatalog(_id, _properties, _resources); } } 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..b6813ec0 100644 --- a/src/extensibility/dotnet-extensibility/Extensibility/DataSource/SimpleDataSource.cs +++ b/src/extensibility/dotnet-extensibility/Extensibility/DataSource/SimpleDataSource.cs @@ -1,78 +1,69 @@ 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 { /// - /// 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!; - - /// - /// 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!; + protected DataSourceContext Context { get; private set; } = default!; - #endregion - - #region Methods - - /// - public Task SetContextAsync( - DataSourceContext context, - ILogger logger, - CancellationToken cancellationToken) - { - Context = context; - Logger = logger; - - return Task.CompletedTask; - } + /// + /// 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!; - /// - public abstract Task GetCatalogRegistrationsAsync( - string path, - CancellationToken cancellationToken); + /// + public Task SetContextAsync( + DataSourceContext context, + ILogger logger, + CancellationToken cancellationToken) + { + Context = context; + Logger = logger; - /// - public abstract Task GetCatalogAsync( - string catalogId, - CancellationToken cancellationToken); + return Task.CompletedTask; + } - /// - public virtual Task<(DateTime Begin, DateTime End)> GetTimeRangeAsync( - string catalogId, - CancellationToken cancellationToken) - { - return Task.FromResult((DateTime.MinValue, DateTime.MaxValue)); - } + /// + public abstract Task GetCatalogRegistrationsAsync( + string path, + CancellationToken cancellationToken); - /// - public virtual Task GetAvailabilityAsync( - string catalogId, - DateTime begin, - DateTime end, - CancellationToken cancellationToken) - { - return Task.FromResult(double.NaN); - } + /// + public abstract Task GetCatalogAsync( + string catalogId, + CancellationToken cancellationToken); - /// - public abstract Task ReadAsync( - DateTime begin, - DateTime end, - ReadRequest[] requests, - ReadDataHandler readData, - IProgress progress, - CancellationToken cancellationToken); + /// + public virtual Task<(DateTime Begin, DateTime End)> GetTimeRangeAsync( + string catalogId, + CancellationToken cancellationToken) + { + return Task.FromResult((DateTime.MinValue, DateTime.MaxValue)); + } - #endregion + /// + 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); } 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..d05e5347 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) - }; + var result1 = new double[86401]; - double[] result1 = new double[86401]; + var writing1 = Task.Run(async () => + { + var 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..]; - } - }); + var result2 = new double[86401]; - double[] result2 = new double[86401]; + var writing2 = Task.Run(async () => + { + var 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); + + var 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(); + var 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/PackageControllerTests.cs b/tests/Nexus.Tests/Other/PackageControllerTests.cs index 5a563438..473324bf 100644 --- a/tests/Nexus.Tests/Other/PackageControllerTests.cs +++ b/tests/Nexus.Tests/Other/PackageControllerTests.cs @@ -12,28 +12,107 @@ 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 // 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 - #region Load [Fact] @@ -252,7 +331,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/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/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/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 49b68113..914a8565 100644 --- a/tests/clients/dotnet-client-tests/ClientTests.cs +++ b/tests/clients/dotnet-client-tests/ClientTests.cs @@ -5,78 +5,78 @@ using Moq.Protected; using Xunit; -namespace Nexus.Api.Tests +namespace Nexus.Api.Tests; + +public class ClientTests { - public class ClientTests - { - public const string NexusConfigurationHeaderKey = "Nexus-Configuration"; + public const string NexusConfigurationHeaderKey = "Nexus-Configuration"; - [Fact] - public async Task CanAddConfigurationAsync() - { - // Arrange - var messageHandlerMock = new Mock(); - var catalogId = "my-catalog-id"; - var expectedCatalog = new ResourceCatalog(Id: catalogId, default, default); + [Fact] + public async Task CanAddConfigurationAsync() + { + // Arrange + var messageHandlerMock = new Mock(); + var catalogId = "my-catalog-id"; + var expectedCatalog = new ResourceCatalog(Id: catalogId, default, default); - var actualHeaders = new List?>(); + var actualHeaders = new List?>(); - messageHandlerMock - .Protected() - .Setup>( - "SendAsync", - ItExpr.IsAny(), - ItExpr.IsAny()) - .Callback((requestMessage, cancellationToken) => - { - requestMessage.Headers.TryGetValues(NexusConfigurationHeaderKey, out var headers); - actualHeaders.Add(headers); - }) - .ReturnsAsync(() => + 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() { - return new HttpResponseMessage() - { - StatusCode = HttpStatusCode.OK, - Content = new StringContent(JsonSerializer.Serialize(expectedCatalog), Encoding.UTF8, "application/json"), - }; - }); + StatusCode = HttpStatusCode.OK, + Content = new StringContent(JsonSerializer.Serialize(expectedCatalog), Encoding.UTF8, "application/json"), + }; + }); - // -> http client - var httpClient = new HttpClient(messageHandlerMock.Object) - { - BaseAddress = new Uri("http://localhost") - }; + // -> http client + var httpClient = new HttpClient(messageHandlerMock.Object) + { + BaseAddress = new Uri("http://localhost") + }; - // -> API client - var client = new NexusClient(httpClient); + // -> API client + var client = new NexusClient(httpClient); - // -> configuration - var configuration = new - { - foo1 = "bar1", - foo2 = "bar2" - }; + // -> configuration + var configuration = new + { + foo1 = "bar1", + foo2 = "bar2" + }; - // Act - _ = await client.Catalogs.GetAsync(catalogId); - - 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 8417dbcb063156e520ce77edd885ce7aaa474afe Mon Sep 17 00:00:00 2001 From: Apollo3zehn <20972129+Apollo3zehn@users.noreply.github.com> Date: Fri, 8 Mar 2024 13:12:04 +0100 Subject: [PATCH 15/19] [Modify] Move login page from wasm client to server (#50) * First draft * Finish * Finish * Fix CSS * Fix API * Fix build --------- Co-authored-by: Apollo3zehn --- .vscode/settings.json | 2 +- openapi.json | 39 ------------ src/Nexus.UI/Components/LoginView.razor | 28 ++++----- src/Nexus.UI/Core/AppState.cs | 4 -- src/Nexus.UI/Core/NexusDemoClient.cs | 10 --- src/Nexus.UI/Program.cs | 4 +- .../NexusAuthenticationStateProvider.cs | 2 +- src/Nexus/API/UsersController.cs | 17 ----- src/Nexus/Core/NexusAuthExtensions.cs | 7 +-- src/Nexus/Pages/Login.cshtml | 62 +++++++++++++++++++ src/Nexus/Program.cs | 34 ++++++++-- src/Nexus/wwwroot/css/app.css | 23 ++----- src/clients/dotnet-client/NexusClient.g.cs | 38 ------------ .../python-client/nexus_api/_nexus_api.py | 39 ------------ 14 files changed, 111 insertions(+), 198 deletions(-) create mode 100644 src/Nexus/Pages/Login.cshtml diff --git a/.vscode/settings.json b/.vscode/settings.json index cd510c2a..81791133 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,5 +8,5 @@ "src/clients/python-client" ], "dotnet.defaultSolution": "Nexus.sln", - "editor.formatOnSave": true + "editor.formatOnSave": false } \ No newline at end of file diff --git a/openapi.json b/openapi.json index b6a73501..e0e6a190 100644 --- a/openapi.json +++ b/openapi.json @@ -1156,30 +1156,6 @@ } } }, - "/api/v1/users/authentication-schemes": { - "get": { - "tags": [ - "Users" - ], - "summary": "Returns a list of available authentication schemes.", - "operationId": "Users_GetAuthenticationSchemes", - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/AuthenticationSchemeDescription" - } - } - } - } - } - } - } - }, "/api/v1/users/authenticate": { "post": { "tags": [ @@ -2177,21 +2153,6 @@ } } }, - "AuthenticationSchemeDescription": { - "type": "object", - "description": "Describes an OpenID connect provider.", - "additionalProperties": false, - "properties": { - "scheme": { - "type": "string", - "description": "The scheme." - }, - "displayName": { - "type": "string", - "description": "The display name." - } - } - }, "MeResponse": { "type": "object", "description": "A me response.", diff --git a/src/Nexus.UI/Components/LoginView.razor b/src/Nexus.UI/Components/LoginView.razor index dd36a6c2..6073a0cc 100644 --- a/src/Nexus.UI/Components/LoginView.razor +++ b/src/Nexus.UI/Components/LoginView.razor @@ -1,22 +1,18 @@ -@using System.Net +@using System.Net -@inject AppState AppState @inject NavigationManager NavigationManager
-
- - Sign-in - -
- @foreach (var scheme in AppState.AuthenticationSchemes) - { -
-
- -
-
- } -
+
+ Sie sind nicht eingeloggt. + + Einloggen +
\ No newline at end of file diff --git a/src/Nexus.UI/Core/AppState.cs b/src/Nexus.UI/Core/AppState.cs index 75e75389..11f26866 100644 --- a/src/Nexus.UI/Core/AppState.cs +++ b/src/Nexus.UI/Core/AppState.cs @@ -27,12 +27,10 @@ public class AppState : INotifyPropertyChanged public AppState( bool isDemo, - IReadOnlyList authenticationSchemes, INexusClient client, IJSInProcessRuntime jsRuntime) { IsDemo = isDemo; - AuthenticationSchemes = authenticationSchemes; _client = client; _jsRuntime = jsRuntime; Settings = new SettingsViewModel(this, jsRuntime, client); @@ -96,8 +94,6 @@ public ViewState ViewState } } - public IReadOnlyList AuthenticationSchemes { get; } - public ExportParameters ExportParameters { get diff --git a/src/Nexus.UI/Core/NexusDemoClient.cs b/src/Nexus.UI/Core/NexusDemoClient.cs index 8f2f9dfd..a58f72c2 100644 --- a/src/Nexus.UI/Core/NexusDemoClient.cs +++ b/src/Nexus.UI/Core/NexusDemoClient.cs @@ -431,16 +431,6 @@ public Task DeleteUserAsync(string userId, CancellationToke throw new NotImplementedException(); } - public IReadOnlyList GetAuthenticationSchemes() - { - throw new NotImplementedException(); - } - - public Task> GetAuthenticationSchemesAsync(CancellationToken cancellationToken = default) - { - return Task.FromResult((IReadOnlyList)new List()); - } - public IReadOnlyDictionary GetClaims(string userId) { throw new NotImplementedException(); diff --git a/src/Nexus.UI/Program.cs b/src/Nexus.UI/Program.cs index 5f307ac4..11e0dd7c 100644 --- a/src/Nexus.UI/Program.cs +++ b/src/Nexus.UI/Program.cs @@ -39,8 +39,6 @@ client = new NexusClient(httpClient); } -var authenticationSchemes = await client.Users.GetAuthenticationSchemesAsync(); - builder.Services .AddCascadingAuthenticationState() .AddAuthorizationCore() @@ -49,7 +47,7 @@ .AddSingleton(serviceProvider => { var jsRuntime = serviceProvider.GetRequiredService(); - var appState = new AppState(isDemo, authenticationSchemes, client, jsRuntime); + var appState = new AppState(isDemo, client, jsRuntime); return appState; }) diff --git a/src/Nexus.UI/Services/NexusAuthenticationStateProvider.cs b/src/Nexus.UI/Services/NexusAuthenticationStateProvider.cs index ef11c96c..07f2753e 100644 --- a/src/Nexus.UI/Services/NexusAuthenticationStateProvider.cs +++ b/src/Nexus.UI/Services/NexusAuthenticationStateProvider.cs @@ -34,7 +34,7 @@ public override async Task GetAuthenticationStateAsync() identity = new ClaimsIdentity( claims, - authenticationType: meResponse.UserId.Split(new[] { '@' }, count: 2)[1], + authenticationType: meResponse.UserId.Split(['@'], count: 2)[1], nameType: NAME_CLAIM, roleType: ROLE_CLAIM); } diff --git a/src/Nexus/API/UsersController.cs b/src/Nexus/API/UsersController.cs index ca37cbfb..12b39cea 100644 --- a/src/Nexus/API/UsersController.cs +++ b/src/Nexus/API/UsersController.cs @@ -25,7 +25,6 @@ namespace Nexus.Controllers; internal class UsersController : ControllerBase { // [anonymous] - // GET /api/users/authentication-schemes // GET /api/users/authenticate // GET /api/users/signout // POST /api/users/tokens/delete @@ -66,22 +65,6 @@ public UsersController( #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 }; - - return providers - .Select(provider => new AuthenticationSchemeDescription(provider.Scheme, provider.DisplayName)) - .ToList(); - } - /// /// Authenticates the user. /// diff --git a/src/Nexus/Core/NexusAuthExtensions.cs b/src/Nexus/Core/NexusAuthExtensions.cs index 412bc378..ec167506 100644 --- a/src/Nexus/Core/NexusAuthExtensions.cs +++ b/src/Nexus/Core/NexusAuthExtensions.cs @@ -8,7 +8,6 @@ using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Nexus.Core; using Nexus.Utilities; -using System.Net; using System.Security.Claims; using static OpenIddict.Abstractions.OpenIddictConstants; @@ -49,11 +48,7 @@ public static IServiceCollection AddNexusAuth( options.ExpireTimeSpan = securityOptions.CookieLifetime; options.SlidingExpiration = false; - options.Events.OnRedirectToAccessDenied = context => - { - context.Response.StatusCode = (int)HttpStatusCode.Forbidden; - return Task.CompletedTask; - }; + options.LoginPath = "/login"; }) .AddScheme( diff --git a/src/Nexus/Pages/Login.cshtml b/src/Nexus/Pages/Login.cshtml new file mode 100644 index 00000000..f196a26c --- /dev/null +++ b/src/Nexus/Pages/Login.cshtml @@ -0,0 +1,62 @@ +@page + +@using Microsoft.Extensions.Options +@using Nexus.Core +@using System.Net + +@inject IOptions SecurityOptions + +@{ + string _returnUrl = WebUtility.UrlEncode(Request.Query["ReturnUrl"].FirstOrDefault() ?? "/"); +} + + + + + + + + Nexus + + + + + + + +
+
+ + + Login + + +
+ @foreach (var provider in SecurityOptions.Value.OidcProviders.Any() ? SecurityOptions.Value.OidcProviders : new List() { NexusAuthExtensions.DefaultProvider }) + { +
+
+ +
+
+ } +
+
+
+ + + \ No newline at end of file diff --git a/src/Nexus/Program.cs b/src/Nexus/Program.cs index cdfec304..7118bdef 100644 --- a/src/Nexus/Program.cs +++ b/src/Nexus/Program.cs @@ -125,10 +125,6 @@ void AddServices( noContentFormatter.TreatNullValueAsNoContent = false; }); - // razor components - services.AddRazorComponents() - .AddInteractiveWebAssemblyComponents(); - // authentication services.AddNexusAuth(pathsOptions, securityOptions); @@ -139,6 +135,26 @@ void AddServices( if (!securityOptions.OidcProviders.Any()) services.AddNexusIdentityProvider(); + // razor components + services.AddRazorComponents() + .AddInteractiveWebAssemblyComponents(); + + /* + * login view: We tried to use Blazor Webs ability to render pages + * on the server but it does not work properly. With the command + * dotnet new blazor --all-interactive --interactivity WebAssembly --no-https + * It is possible to simply define server side razor pages without + * any changes and because prerendering is enabled by default it is + * being displayed shortly but then Blazor starts and redirects + * the user to a "Not found" page. + * + * Related issue: + * https://github.com/dotnet/aspnetcore/issues/51046 + */ + + // razor pages (for login view) + services.AddRazorPages(); + // routing services.AddRouting(options => options.LowercaseUrls = true); @@ -222,13 +238,21 @@ void ConfigurePipeline(WebApplication app) app.UseAuthorization(); // endpoints + + /* REST API */ app.MapControllers(); + /* Login view */ + app.MapRazorPages(); + + /* Debugging (print all routes) */ + app.MapGet("/debug/routes", (IEnumerable endpointSources) => + string.Join("\n", endpointSources.SelectMany(source => source.Endpoints))); + // razor components app.MapRazorComponents() .AddInteractiveWebAssemblyRenderMode() .AddAdditionalAssemblies(typeof(MainLayout).Assembly); - } async Task InitializeAppAsync( diff --git a/src/Nexus/wwwroot/css/app.css b/src/Nexus/wwwroot/css/app.css index b5c750b4..6bc60541 100644 --- a/src/Nexus/wwwroot/css/app.css +++ b/src/Nexus/wwwroot/css/app.css @@ -664,10 +664,6 @@ video { margin-bottom: 0.75rem; } -.mb-5 { - margin-bottom: 1.25rem; -} - .mb-7 { margin-bottom: 1.75rem; } @@ -913,6 +909,10 @@ video { gap: 1.25rem; } +.gap-7 { + gap: 1.75rem; +} + .overflow-auto { overflow: auto; } @@ -1131,11 +1131,6 @@ video { padding: 1.25rem; } -.px-10 { - padding-left: 2.5rem; - padding-right: 2.5rem; -} - .px-2 { padding-left: 0.5rem; padding-right: 0.5rem; @@ -1282,11 +1277,6 @@ video { color: rgb(14 116 144 / var(--tw-text-opacity)); } -.text-cyan-800 { - --tw-text-opacity: 1; - color: rgb(21 94 117 / var(--tw-text-opacity)); -} - .text-gray-100 { --tw-text-opacity: 1; color: rgb(243 244 246 / var(--tw-text-opacity)); @@ -1543,11 +1533,6 @@ html { transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); } -.hover\:border-cyan-700:hover { - --tw-border-opacity: 1; - border-color: rgb(14 116 144 / var(--tw-border-opacity)); -} - .hover\:border-gray-400:hover { --tw-border-opacity: 1; border-color: rgb(156 163 175 / var(--tw-border-opacity)); diff --git a/src/clients/dotnet-client/NexusClient.g.cs b/src/clients/dotnet-client/NexusClient.g.cs index cbdc1f73..684446af 100644 --- a/src/clients/dotnet-client/NexusClient.g.cs +++ b/src/clients/dotnet-client/NexusClient.g.cs @@ -2164,17 +2164,6 @@ public Task SetConfigurationAsync(IReadOnlyDictionary? conf /// public interface IUsersClient { - /// - /// Returns a list of available authentication schemes. - /// - IReadOnlyList GetAuthenticationSchemes(); - - /// - /// Returns a list of available authentication schemes. - /// - /// The token to cancel the current operation. - Task> GetAuthenticationSchemesAsync(CancellationToken cancellationToken = default); - /// /// Authenticates the user. /// @@ -2371,26 +2360,6 @@ internal UsersClient(NexusClient client) ___client = client; } - /// - public IReadOnlyList GetAuthenticationSchemes() - { - var __urlBuilder = new StringBuilder(); - __urlBuilder.Append("/api/v1/users/authentication-schemes"); - - var __url = __urlBuilder.ToString(); - return ___client.Invoke>("GET", __url, "application/json", default, default); - } - - /// - public Task> GetAuthenticationSchemesAsync(CancellationToken cancellationToken = default) - { - var __urlBuilder = new StringBuilder(); - __urlBuilder.Append("/api/v1/users/authentication-schemes"); - - var __url = __urlBuilder.ToString(); - return ___client.InvokeAsync>("GET", __url, "application/json", default, default, cancellationToken); - } - /// public HttpResponseMessage Authenticate(string scheme, string returnUrl) { @@ -3105,13 +3074,6 @@ public record ExtensionDescription(string Type, string Version, string? Descript /// 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, string? ReleasePattern, string? VisibilityPattern); -/// -/// Describes an OpenID connect provider. -/// -/// The scheme. -/// The display name. -public record AuthenticationSchemeDescription(string Scheme, string DisplayName); - /// /// A me response. /// diff --git a/src/clients/python-client/nexus_api/_nexus_api.py b/src/clients/python-client/nexus_api/_nexus_api.py index 29b217bb..19ba6bf4 100644 --- a/src/clients/python-client/nexus_api/_nexus_api.py +++ b/src/clients/python-client/nexus_api/_nexus_api.py @@ -696,23 +696,6 @@ class DataSourceRegistration: """An optional regular expressions pattern to select the catalogs to be visible. By default, all catalogs will be visible.""" -@dataclass(frozen=True) -class AuthenticationSchemeDescription: - """ - Describes an OpenID connect provider. - - Args: - scheme: The scheme. - display_name: The display name. - """ - - scheme: str - """The scheme.""" - - display_name: str - """The display name.""" - - @dataclass(frozen=True) class MeResponse: """ @@ -1339,17 +1322,6 @@ class UsersAsyncClient: def __init__(self, client: NexusAsyncClient): self.___client = client - def get_authentication_schemes(self) -> Awaitable[list[AuthenticationSchemeDescription]]: - """ - Returns a list of available authentication schemes. - - Args: - """ - - __url = "/api/v1/users/authentication-schemes" - - return self.___client._invoke(list[AuthenticationSchemeDescription], "GET", __url, "application/json", None, None) - def authenticate(self, scheme: str, return_url: str) -> Awaitable[Response]: """ Authenticates the user. @@ -2114,17 +2086,6 @@ class UsersClient: def __init__(self, client: NexusClient): self.___client = client - def get_authentication_schemes(self) -> list[AuthenticationSchemeDescription]: - """ - Returns a list of available authentication schemes. - - Args: - """ - - __url = "/api/v1/users/authentication-schemes" - - return self.___client._invoke(list[AuthenticationSchemeDescription], "GET", __url, "application/json", None, None) - def authenticate(self, scheme: str, return_url: str) -> Response: """ Authenticates the user. From a5a786eae61cfde81efc4a655a1a93f53ee100d6 Mon Sep 17 00:00:00 2001 From: Apollo3zehn Date: Fri, 15 Mar 2024 10:04:11 +0100 Subject: [PATCH 16/19] Clean up --- src/Nexus/Program.cs | 3 +-- .../Extensibility/ExtensibilityUtilities.cs | 5 ----- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/src/Nexus/Program.cs b/src/Nexus/Program.cs index 7118bdef..5709ad20 100644 --- a/src/Nexus/Program.cs +++ b/src/Nexus/Program.cs @@ -76,7 +76,7 @@ ConfigurePipeline(app); // initialize app state - await InitializeAppAsync(app.Services, pathsOptions, securityOptions, app.Logger); + await InitializeAppAsync(app.Services, pathsOptions, app.Logger); // Run app.Run(); @@ -258,7 +258,6 @@ void ConfigurePipeline(WebApplication app) async Task InitializeAppAsync( IServiceProvider serviceProvider, PathsOptions pathsOptions, - SecurityOptions securityOptions, ILogger logger) { var appState = serviceProvider.GetRequiredService(); diff --git a/src/extensibility/dotnet-extensibility/Extensibility/ExtensibilityUtilities.cs b/src/extensibility/dotnet-extensibility/Extensibility/ExtensibilityUtilities.cs index 1246481d..6ffa7077 100644 --- a/src/extensibility/dotnet-extensibility/Extensibility/ExtensibilityUtilities.cs +++ b/src/extensibility/dotnet-extensibility/Extensibility/ExtensibilityUtilities.cs @@ -34,9 +34,4 @@ internal static int CalculateElementCount(DateTime begin, DateTime end, TimeSpan { 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); - } } \ No newline at end of file From 40d1cea5c8f9f2b0909316a126e61ccbc2d9e289 Mon Sep 17 00:00:00 2001 From: Apollo3zehn Date: Fri, 15 Mar 2024 17:23:55 +0100 Subject: [PATCH 17/19] Clean up --- .vscode/launch.json | 2 +- src/Nexus/Nexus.csproj | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 4cc1749a..29759e7a 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -46,7 +46,7 @@ "pipeProgram": "ssh", "pipeArgs": [ "-T", - "root@ensyno.iwes.fraunhofer.de", + "root@", "-p", "2222" ], // replace diff --git a/src/Nexus/Nexus.csproj b/src/Nexus/Nexus.csproj index 85669cad..2e523a55 100644 --- a/src/Nexus/Nexus.csproj +++ b/src/Nexus/Nexus.csproj @@ -6,7 +6,6 @@ - From 077dcd4cd5b663e437682e1855b6e6e1cce491a5 Mon Sep 17 00:00:00 2001 From: Apollo3zehn Date: Fri, 15 Mar 2024 20:41:31 +0100 Subject: [PATCH 18/19] Fix catalogs not being equal --- src/Nexus/Extensions/Writers/Csv.cs | 6 +++--- .../dotnet-extensibility/DataModel/ResourceCatalog.cs | 4 ++-- tests/Nexus.Tests/Services/DataServiceTests.cs | 4 ++-- .../dotnet-extensibility-tests/DataModelTests.cs | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Nexus/Extensions/Writers/Csv.cs b/src/Nexus/Extensions/Writers/Csv.cs index 516e5be9..702e6ec9 100644 --- a/src/Nexus/Extensions/Writers/Csv.cs +++ b/src/Nexus/Extensions/Writers/Csv.cs @@ -203,7 +203,7 @@ public async Task WriteAsync(TimeSpan fileOffset, WriteRequest[] requests, IProg var offset = fileOffset.Ticks / _lastSamplePeriod.Ticks; var requestGroups = requests - .GroupBy(request => request.CatalogItem.Catalog) + .GroupBy(request => request.CatalogItem.Catalog.Id) .ToList(); var rowIndexFormat = Context.RequestConfiguration?.GetStringValue("row-index-format") ?? "index"; @@ -219,9 +219,9 @@ public async Task WriteAsync(TimeSpan fileOffset, WriteRequest[] requests, IProg { cancellationToken.ThrowIfCancellationRequested(); - var catalog = requestGroup.Key; + var catalogId = requestGroup.Key; var writeRequests = requestGroup.ToArray(); - var physicalId = catalog.Id.TrimStart('/').Replace('/', '_'); + var physicalId = catalogId.TrimStart('/').Replace('/', '_'); var root = Context.ResourceLocator.ToPath(); var filePath = Path.Combine(root, $"{physicalId}_{ToISO8601(_lastFileBegin)}_{_lastSamplePeriod.ToUnitString()}.csv"); diff --git a/src/extensibility/dotnet-extensibility/DataModel/ResourceCatalog.cs b/src/extensibility/dotnet-extensibility/DataModel/ResourceCatalog.cs index 941c6667..43d6824d 100644 --- a/src/extensibility/dotnet-extensibility/DataModel/ResourceCatalog.cs +++ b/src/extensibility/dotnet-extensibility/DataModel/ResourceCatalog.cs @@ -169,8 +169,8 @@ internal bool TryFind(ResourcePathParseResult parseResult, [NotNullWhen(true)] o return false; catalogItem = new CatalogItem( - this with { Resources = default }, - resource with { Representations = default }, + this, + resource, representation, parameters); diff --git a/tests/Nexus.Tests/Services/DataServiceTests.cs b/tests/Nexus.Tests/Services/DataServiceTests.cs index 032e623c..4462b94b 100644 --- a/tests/Nexus.Tests/Services/DataServiceTests.cs +++ b/tests/Nexus.Tests/Services/DataServiceTests.cs @@ -41,9 +41,9 @@ public async Task CanExportAsync() .Callback, CancellationToken>( (begin, end, samplePeriod, filePeriod, catalogItemRequestPipeReaders, progress, cancellationToken) => { - foreach (var catalogItemRequestPipeReaderGroup in catalogItemRequestPipeReaders.GroupBy(x => x.Request.Item.Catalog)) + foreach (var catalogIdRequestPipeReaderGroup in catalogItemRequestPipeReaders.GroupBy(x => x.Request.Item.Catalog.Id)) { - var prefix = catalogItemRequestPipeReaderGroup.Key.Id.TrimStart('/').Replace('/', '_'); + var prefix = catalogIdRequestPipeReaderGroup.Key.TrimStart('/').Replace('/', '_'); var filePath = Path.Combine(tmpUri.LocalPath, $"{prefix}.dat"); File.Create(filePath).Dispose(); } diff --git a/tests/extensibility/dotnet-extensibility-tests/DataModelTests.cs b/tests/extensibility/dotnet-extensibility-tests/DataModelTests.cs index 10ff118f..03607a61 100644 --- a/tests/extensibility/dotnet-extensibility-tests/DataModelTests.cs +++ b/tests/extensibility/dotnet-extensibility-tests/DataModelTests.cs @@ -283,8 +283,8 @@ public void CanTryFindCatalogItem() var catalog = new ResourceCatalog(id: "/A/B/C", resources: new List() { resource }); var catalogItem = new CatalogItem( - catalog with { Resources = default }, - resource with { Representations = default }, + catalog, + resource, representation, Parameters: default); From 16836f262ac39918ada311c70a433f79e1ee7e8b Mon Sep 17 00:00:00 2001 From: Apollo3zehn Date: Fri, 15 Mar 2024 21:12:57 +0100 Subject: [PATCH 19/19] Apply code style --- src/Nexus.UI/Charts/Chart.razor.cs | 10 ++-- src/Nexus.UI/Core/AppState.cs | 14 ++--- src/Nexus.UI/Core/NexusDemoClient.cs | 10 ++-- src/Nexus.UI/Core/Utilities.cs | 15 +++-- src/Nexus.UI/Pages/ChartTest.razor.cs | 6 +- .../NexusAuthenticationStateProvider.cs | 11 +--- src/Nexus.UI/Services/TypeFaceService.cs | 6 +- .../CatalogItemSelectionViewModel.cs | 18 ++---- src/Nexus.UI/ViewModels/SettingsViewModel.cs | 8 +-- src/Nexus/API/ArtifactsController.cs | 11 +--- src/Nexus/API/CatalogsController.cs | 21 +++---- src/Nexus/API/DataController.cs | 11 +--- src/Nexus/API/JobsController.cs | 55 +++++------------- src/Nexus/API/PackageReferencesController.cs | 21 +++---- src/Nexus/API/SourcesController.cs | 21 +++---- src/Nexus/API/SystemController.cs | 21 +++---- src/Nexus/API/UsersController.cs | 26 +++------ src/Nexus/API/WritersController.cs | 11 +--- src/Nexus/Core/CatalogContainer.cs | 7 +-- src/Nexus/Core/CustomExtensions.cs | 3 +- src/Nexus/Core/Models_Public.cs | 45 ++++----------- src/Nexus/Core/NexusAuthExtensions.cs | 21 +++---- .../Core/NexusIdentityProviderExtensions.cs | 9 +-- src/Nexus/Core/NexusOptions.cs | 2 +- ...ersonalAccessTokenAuthenticationHandler.cs | 25 +++----- src/Nexus/Core/UserDbContext.cs | 9 +-- .../DataSource/DataSourceController.cs | 57 +++++++------------ .../DataSourceControllerExtensions.cs | 8 +-- .../DataSource/DataSourceDoubleStream.cs | 16 ++---- .../DataWriter/DataWriterController.cs | 31 ++++------ src/Nexus/Extensions/Sources/Sample.cs | 6 +- src/Nexus/Extensions/Writers/Csv.cs | 6 +- .../PackageManagement/PackageController.cs | 50 +++++----------- .../PackageManagement/PackageLoadContext.cs | 10 +--- src/Nexus/Program.cs | 2 +- src/Nexus/Services/AppStateManager.cs | 37 ++++-------- src/Nexus/Services/CacheService.cs | 11 +--- src/Nexus/Services/CatalogManager.cs | 42 ++++++-------- src/Nexus/Services/DataControllerService.cs | 46 ++++++--------- src/Nexus/Services/DataService.cs | 54 ++++++------------ src/Nexus/Services/DatabaseService.cs | 10 +--- src/Nexus/Services/DbService.cs | 11 +--- src/Nexus/Services/ExtensionHive.cs | 23 +++----- src/Nexus/Services/MemoryTracker.cs | 16 ++---- src/Nexus/Services/ProcessingService.cs | 18 +++--- src/Nexus/Services/TokenService.cs | 12 ++-- src/Nexus/Utilities/MemoryManager.cs | 6 +- src/Nexus/Utilities/NexusUtilities.cs | 9 ++- .../DataModel/DataModelExtensions.cs | 13 +++-- .../DataModel/DataModelUtilities.cs | 7 ++- .../DataModel/Representation.cs | 7 ++- .../DataModel/Resource.cs | 7 ++- .../DataModel/ResourceBuilder.cs | 6 +- .../DataModel/ResourceCatalog.cs | 7 ++- .../DataModel/ResourceCatalogBuilder.cs | 8 +-- .../DataWriter/DataWriterTypes.cs | 15 ++--- .../ExtensionDescriptionAttribute.cs | 23 +++----- .../DataSource/DataSourceControllerTests.cs | 20 +++---- .../DataSource/SampleDataSourceTests.cs | 4 +- .../DataWriter/CsvDataWriterTests.cs | 10 +--- .../DataWriter/DataWriterControllerTests.cs | 10 +--- .../DataWriter/DataWriterFixture.cs | 8 +-- .../Other/CatalogContainersExtensionsTests.cs | 28 ++++----- tests/Nexus.Tests/Other/LoggingTests.cs | 2 +- tests/Nexus.Tests/Other/OptionsTests.cs | 12 ++-- .../Other/PackageControllerTests.cs | 6 +- .../Nexus.Tests/Services/CacheServiceTests.cs | 6 +- .../Services/CatalogManagerTests.cs | 14 ++--- .../Nexus.Tests/Services/DataServiceTests.cs | 2 +- .../Services/MemoryTrackerTests.cs | 4 +- .../Nexus.Tests/Services/TokenServiceTests.cs | 38 ++++++------- .../dotnet-client-tests/ClientTests.cs | 3 +- .../DataModelTests.cs | 28 ++++----- 73 files changed, 445 insertions(+), 741 deletions(-) diff --git a/src/Nexus.UI/Charts/Chart.razor.cs b/src/Nexus.UI/Charts/Chart.razor.cs index 0e9d358d..1f6e80dc 100644 --- a/src/Nexus.UI/Charts/Chart.razor.cs +++ b/src/Nexus.UI/Charts/Chart.razor.cs @@ -54,8 +54,8 @@ public Chart() { _dotNetHelper = DotNetObjectReference.Create(this); - _timeAxisConfigs = new[] - { + _timeAxisConfigs = + [ /* nanoseconds */ new TimeAxisConfig(TimeSpan.FromSeconds(100e-9), ".fffffff", TriggerPeriod.Second, "HH:mm.ss", "yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss.fffffff"), @@ -101,11 +101,11 @@ public Chart() /* years */ new TimeAxisConfig(TimeSpan.FromDays(365), "yyyy", TriggerPeriod.Year, default, default, "yyyy-MM-dd"), - }; + ]; _timeAxisConfig = _timeAxisConfigs.First(); - _colors = new[] { + _colors = [ new SKColor(0, 114, 189), new SKColor(217, 83, 25), new SKColor(237, 177, 32), @@ -113,7 +113,7 @@ public Chart() new SKColor(119, 172, 48), new SKColor(77, 190, 238), new SKColor(162, 20, 47) - }; + ]; } [Inject] diff --git a/src/Nexus.UI/Core/AppState.cs b/src/Nexus.UI/Core/AppState.cs index 11f26866..945c98b7 100644 --- a/src/Nexus.UI/Core/AppState.cs +++ b/src/Nexus.UI/Core/AppState.cs @@ -17,8 +17,8 @@ public class AppState : INotifyPropertyChanged private ViewState _viewState = ViewState.Normal; private ExportParameters _exportParameters = default!; private readonly INexusClient _client; - private readonly List<(DateTime, Exception)> _errors = new(); - private readonly Dictionary> _editModeCatalogMap = new(); + private readonly List<(DateTime, Exception)> _errors = []; + private readonly Dictionary> _editModeCatalogMap = []; private bool _beginAtZero; private string? _searchString; private const string GROUP_KEY = "groups"; @@ -176,7 +176,7 @@ public string? SearchString } } - public ObservableCollection Jobs { get; set; } = new ObservableCollection(); + public ObservableCollection Jobs { get; set; } = []; public void AddJob(JobViewModel job) { @@ -232,7 +232,7 @@ public void AddError(Exception error, ISnackbar? snackbar) public void AddEditModeCatalog(string catalogId) { - _editModeCatalogMap.Add(catalogId, new Dictionary()); + _editModeCatalogMap.Add(catalogId, []); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(EditModeCatalogMap))); } @@ -258,7 +258,7 @@ public async Task SaveAndRemoveEditModeCatalogAsync(string catalogId, ISnackbar resources = resourcesNode.AsArray(); else - resources = new JsonArray(); + resources = []; overrides["Resources"] = resources; @@ -283,7 +283,7 @@ public async Task SaveAndRemoveEditModeCatalogAsync(string catalogId, ISnackbar properties = propertiesNode.AsObject(); else - properties = new JsonObject(); + properties = []; resource["Properties"] = properties; @@ -351,7 +351,7 @@ public async Task SaveAndRemoveEditModeCatalogAsync(string catalogId, ISnackbar if (!success) { - group = new List(); + group = []; catalogItemsMap[groupName] = group; } diff --git a/src/Nexus.UI/Core/NexusDemoClient.cs b/src/Nexus.UI/Core/NexusDemoClient.cs index a58f72c2..9faf40b7 100644 --- a/src/Nexus.UI/Core/NexusDemoClient.cs +++ b/src/Nexus.UI/Core/NexusDemoClient.cs @@ -72,7 +72,7 @@ public Task GetAsync(string catalogId, CancellationToken cancel var resource1 = new Resource( Id: "temperature", Properties: properties1, - Representations: new List() { new Representation(NexusDataType.FLOAT64, TimeSpan.FromMinutes(1), default) } + Representations: new List() { new(NexusDataType.FLOAT64, TimeSpan.FromMinutes(1), default) } ); var properties2 = new Dictionary() @@ -85,7 +85,7 @@ public Task GetAsync(string catalogId, CancellationToken cancel var resource2 = new Resource( Id: "wind_speed", Properties: properties2, - Representations: new List() { new Representation(NexusDataType.FLOAT64, TimeSpan.FromMinutes(1), default) } + Representations: new List() { new(NexusDataType.FLOAT64, TimeSpan.FromMinutes(1), default) } ); var resources = new List() { resource1, resource2 }; @@ -188,12 +188,12 @@ We hope you enjoy it! PackageReferenceId: Guid.NewGuid() ); - return Task.FromResult((IReadOnlyList)new List() { catalogInfo }); + return Task.FromResult((IReadOnlyList)[catalogInfo]); } else { - return Task.FromResult((IReadOnlyList)new List() { }); + return Task.FromResult((IReadOnlyList)[]); } } @@ -539,6 +539,6 @@ public Task> GetDescriptionsAsync(Cancellati AdditionalInformation: additionalInformation ); - return Task.FromResult((IReadOnlyList)new List() { description }); + return Task.FromResult((IReadOnlyList)[description]); } } \ No newline at end of file diff --git a/src/Nexus.UI/Core/Utilities.cs b/src/Nexus.UI/Core/Utilities.cs index 52a1d7e7..5fccc74e 100644 --- a/src/Nexus.UI/Core/Utilities.cs +++ b/src/Nexus.UI/Core/Utilities.cs @@ -16,7 +16,7 @@ public record ResourcePathParseResult( TimeSpan? BasePeriod ); -public static class Utilities +public static partial class Utilities { public static string ToSpaceFilledCatalogId(string catalogId) => catalogId.TrimStart('/').Replace("/", " / "); @@ -26,11 +26,11 @@ public static string EscapeDataString(string catalogId) // keep in sync with DataModelExtensions ... 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" }; + private static readonly long[] _nanoseconds = [(long)1e0, (long)1e3, (long)1e6, (long)1e9, (long)60e9, (long)3600e9, (long)86400e9]; + private static readonly int[] _quotients = [1000, 1000, 1000, 60, 60, 24, 1]; + private static readonly string[] _postFixes = ["ns", "us", "ms", "s", "min", "h", "d"]; // ... except this line - private static readonly Regex _unitStringEvaluator = new(@"^\s*([0-9]+)[\s_]*([a-zA-Z]+)\s*$", RegexOptions.Compiled); + private static readonly Regex _unitStringEvaluator = UnitStringEvaluator(); public static string ToUnitString(this TimeSpan samplePeriod, bool withUnderScore = false) { @@ -155,7 +155,7 @@ public static void ParseResourcePath( var matches = _matchSingleParametersExpression .Matches(parseResult.Parameters); - if (matches.Any()) + if (matches.Count != 0) { parameters = new ReadOnlyDictionary(matches .Select(match => (match.Groups[1].Value, match.Groups[2].Value)) @@ -331,4 +331,7 @@ private static JsonElement GetJsonObjectFromPath(this JsonElement root, Span value / 4.0).ToArray()), - new LineSeries( + new( "Temperature", "°C", TimeSpan.FromSeconds(1), Enumerable.Range(0, 60).Select(value => random.NextDouble() * 10 - 5).ToArray()), - new LineSeries( + new( "Pressure", "mbar", TimeSpan.FromSeconds(1), diff --git a/src/Nexus.UI/Services/NexusAuthenticationStateProvider.cs b/src/Nexus.UI/Services/NexusAuthenticationStateProvider.cs index 07f2753e..28e5188d 100644 --- a/src/Nexus.UI/Services/NexusAuthenticationStateProvider.cs +++ b/src/Nexus.UI/Services/NexusAuthenticationStateProvider.cs @@ -4,14 +4,9 @@ namespace Nexus.UI.Services; -public class NexusAuthenticationStateProvider : AuthenticationStateProvider +public class NexusAuthenticationStateProvider(INexusClient client) : AuthenticationStateProvider { - private readonly INexusClient _client; - - public NexusAuthenticationStateProvider(INexusClient client) - { - _client = client; - } + private readonly INexusClient _client = client; public override async Task GetAuthenticationStateAsync() { @@ -26,7 +21,7 @@ public override async Task GetAuthenticationStateAsync() var claims = new List { - new Claim(NAME_CLAIM, meResponse.User.Name) + new(NAME_CLAIM, meResponse.User.Name) }; if (meResponse.IsAdmin) diff --git a/src/Nexus.UI/Services/TypeFaceService.cs b/src/Nexus.UI/Services/TypeFaceService.cs index 9144d19d..a625af5c 100644 --- a/src/Nexus.UI/Services/TypeFaceService.cs +++ b/src/Nexus.UI/Services/TypeFaceService.cs @@ -8,12 +8,12 @@ namespace Nexus.UI.Services; public class TypeFaceService { - private readonly Dictionary _typeFaces = new(); + private readonly Dictionary _typeFaces = []; public SKTypeface GetTTF(string ttfName) { - if (_typeFaces.ContainsKey(ttfName)) - return _typeFaces[ttfName]; + if (_typeFaces.TryGetValue(ttfName, out var value)) + return value; else if (LoadTypeFace(ttfName)) return _typeFaces[ttfName]; diff --git a/src/Nexus.UI/ViewModels/CatalogItemSelectionViewModel.cs b/src/Nexus.UI/ViewModels/CatalogItemSelectionViewModel.cs index 81f6d86f..2f96b370 100644 --- a/src/Nexus.UI/ViewModels/CatalogItemSelectionViewModel.cs +++ b/src/Nexus.UI/ViewModels/CatalogItemSelectionViewModel.cs @@ -2,19 +2,13 @@ namespace Nexus.UI.ViewModels; -public class CatalogItemSelectionViewModel +public class CatalogItemSelectionViewModel( + CatalogItemViewModel baseItem, + IDictionary? parameters) { - public CatalogItemSelectionViewModel( - CatalogItemViewModel baseItem, - IDictionary? parameters) - { - BaseItem = baseItem; - Parameters = parameters; - } - - public CatalogItemViewModel BaseItem { get; } - public IDictionary? Parameters { get; } - public List Kinds { get; } = new List(); + public CatalogItemViewModel BaseItem { get; } = baseItem; + public IDictionary? Parameters { get; } = parameters; + public List Kinds { get; } = []; public string GetResourcePath(RepresentationKind kind, TimeSpan samplePeriod) { diff --git a/src/Nexus.UI/ViewModels/SettingsViewModel.cs b/src/Nexus.UI/ViewModels/SettingsViewModel.cs index db3a4056..353f6916 100644 --- a/src/Nexus.UI/ViewModels/SettingsViewModel.cs +++ b/src/Nexus.UI/ViewModels/SettingsViewModel.cs @@ -14,7 +14,7 @@ public class SettingsViewModel : INotifyPropertyChanged private readonly AppState _appState; private readonly INexusClient _client; private readonly IJSInProcessRuntime _jsRuntime; - private List _selectedCatalogItems = new(); + private List _selectedCatalogItems = []; public SettingsViewModel(AppState appState, IJSInProcessRuntime jsRuntime, INexusClient client) { @@ -209,7 +209,7 @@ public void ToggleCatalogItemSelection(CatalogItemSelectionViewModel selection) if (reference is null) { - if (CanModifySamplePeriod && !_selectedCatalogItems.Any()) + if (CanModifySamplePeriod && _selectedCatalogItems.Count == 0) SamplePeriod = new Period(selection.BaseItem.Representation.SamplePeriod); EnsureDefaultRepresentationKind(selection); @@ -244,7 +244,7 @@ private void EnsureDefaultRepresentationKind(CatalogItemSelectionViewModel selec var baseItem = selectedItem.BaseItem; var baseSamplePeriod = baseItem.Representation.SamplePeriod; - if (!selectedItem.Kinds.Any()) + if (selectedItem.Kinds.Count == 0) { if (SamplePeriod.Value < baseSamplePeriod) selectedItem.Kinds.Add(RepresentationKind.Resampled); @@ -268,7 +268,7 @@ private async Task InitializeAsync() .Where(description => description.AdditionalInformation.GetStringValue(Constants.DATA_WRITER_LABEL_KEY) is not null) .ToList(); - if (writerDescriptions.Any()) + if (writerDescriptions.Count != 0) { string? actualFileType = default; diff --git a/src/Nexus/API/ArtifactsController.cs b/src/Nexus/API/ArtifactsController.cs index e04aea59..447f9a3c 100644 --- a/src/Nexus/API/ArtifactsController.cs +++ b/src/Nexus/API/ArtifactsController.cs @@ -11,17 +11,12 @@ namespace Nexus.Controllers; [ApiController] [ApiVersion("1.0")] [Route("api/v{version:apiVersion}/[controller]")] -internal class ArtifactsController : ControllerBase +internal class ArtifactsController( + IDatabaseService databaseService) : ControllerBase { // GET /api/artifacts/{artifactId} - public IDatabaseService _databaseService; - - public ArtifactsController( - IDatabaseService databaseService) - { - _databaseService = databaseService; - } + public IDatabaseService _databaseService = databaseService; /// /// Gets the specified artifact. diff --git a/src/Nexus/API/CatalogsController.cs b/src/Nexus/API/CatalogsController.cs index 8e50f36c..c1e24c4b 100644 --- a/src/Nexus/API/CatalogsController.cs +++ b/src/Nexus/API/CatalogsController.cs @@ -22,7 +22,10 @@ namespace Nexus.Controllers; [ApiController] [ApiVersion("1.0")] [Route("api/v{version:apiVersion}/[controller]")] -internal class CatalogsController : ControllerBase +internal class CatalogsController( + AppState appState, + IDatabaseService databaseService, + IDataControllerService dataControllerService) : ControllerBase { // POST /api/catalogs/search-items // GET /api/catalogs/{catalogId} @@ -38,19 +41,9 @@ internal class CatalogsController : ControllerBase // GET /api/catalogs/{catalogId}/metadata // PUT /api/catalogs/{catalogId}/metadata - private readonly AppState _appState; - private readonly IDatabaseService _databaseService; - private readonly IDataControllerService _dataControllerService; - - public CatalogsController( - AppState appState, - IDatabaseService databaseService, - IDataControllerService dataControllerService) - { - _appState = appState; - _databaseService = databaseService; - _dataControllerService = dataControllerService; - } + private readonly AppState _appState = appState; + private readonly IDatabaseService _databaseService = databaseService; + private readonly IDataControllerService _dataControllerService = dataControllerService; /// /// Searches for the given resource paths and returns the corresponding catalog items. diff --git a/src/Nexus/API/DataController.cs b/src/Nexus/API/DataController.cs index 23b4969e..1ce32ff7 100644 --- a/src/Nexus/API/DataController.cs +++ b/src/Nexus/API/DataController.cs @@ -14,17 +14,12 @@ namespace Nexus.Controllers; [ApiController] [ApiVersion("1.0")] [Route("api/v{version:apiVersion}/[controller]")] -internal class DataController : ControllerBase +internal class DataController( + IDataService dataService) : ControllerBase { // GET /api/data - private readonly IDataService _dataService; - - public DataController( - IDataService dataService) - { - _dataService = dataService; - } + private readonly IDataService _dataService = dataService; /// /// Gets the requested data. diff --git a/src/Nexus/API/JobsController.cs b/src/Nexus/API/JobsController.cs index 99e09471..3280452f 100644 --- a/src/Nexus/API/JobsController.cs +++ b/src/Nexus/API/JobsController.cs @@ -15,7 +15,12 @@ namespace Nexus.Controllers; [ApiController] [ApiVersion("1.0")] [Route("api/v{version:apiVersion}/[controller]")] -internal class JobsController : ControllerBase +internal class JobsController( + AppStateManager appStateManager, + IJobService jobService, + IServiceProvider serviceProvider, + Serilog.IDiagnosticContext diagnosticContext, + ILogger logger) : ControllerBase { // GET /jobs // DELETE /jobs{jobId} @@ -24,25 +29,11 @@ internal class JobsController : ControllerBase // POST /jobs/load-packages // POST /jobs/clear-cache - private readonly AppStateManager _appStateManager; - private readonly ILogger _logger; - private readonly IServiceProvider _serviceProvider; - private readonly Serilog.IDiagnosticContext _diagnosticContext; - private readonly IJobService _jobService; - - public JobsController( - AppStateManager appStateManager, - IJobService jobService, - IServiceProvider serviceProvider, - Serilog.IDiagnosticContext diagnosticContext, - ILogger logger) - { - _appStateManager = appStateManager; - _jobService = jobService; - _serviceProvider = serviceProvider; - _diagnosticContext = diagnosticContext; - _logger = logger; - } + private readonly AppStateManager _appStateManager = appStateManager; + private readonly ILogger _logger = logger; + private readonly IServiceProvider _serviceProvider = serviceProvider; + private readonly Serilog.IDiagnosticContext _diagnosticContext = diagnosticContext; + private readonly IJobService _jobService = jobService; #region Jobs Management @@ -54,11 +45,7 @@ public JobsController( public ActionResult> GetJobs() { var isAdmin = User.IsInRole(NexusRoles.ADMINISTRATOR); - var username = User.Identity?.Name; - - if (username is null) - throw new Exception("This should never happen."); - + var username = (User.Identity?.Name) ?? throw new Exception("This should never happen."); var result = _jobService .GetJobs() .Select(jobControl => jobControl.Job) @@ -79,11 +66,7 @@ public ActionResult CancelJob(Guid jobId) if (_jobService.TryGetJob(jobId, out var jobControl)) { var isAdmin = User.IsInRole(NexusRoles.ADMINISTRATOR); - var username = User.Identity?.Name; - - if (username is null) - throw new Exception("This should never happen."); - + var username = (User.Identity?.Name) ?? throw new Exception("This should never happen."); if (jobControl.Job.Owner == username || isAdmin) { jobControl.CancellationTokenSource.Cancel(); @@ -113,11 +96,7 @@ 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 (username is null) - throw new Exception("This should never happen."); - + var username = (User.Identity?.Name) ?? throw new Exception("This should never happen."); if (jobControl.Job.Owner == username || isAdmin) { var status = new JobStatus( @@ -180,11 +159,7 @@ public async Task> ExportAsync( { catalogItemRequests = await Task.WhenAll(parameters.ResourcePaths.Select(async resourcePath => { - var catalogItemRequest = await root.TryFindAsync(resourcePath, cancellationToken); - - if (catalogItemRequest is null) - 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 catalogItemRequest; })); } diff --git a/src/Nexus/API/PackageReferencesController.cs b/src/Nexus/API/PackageReferencesController.cs index 022d801e..7fa458ac 100644 --- a/src/Nexus/API/PackageReferencesController.cs +++ b/src/Nexus/API/PackageReferencesController.cs @@ -12,26 +12,19 @@ namespace Nexus.Controllers; [ApiController] [ApiVersion("1.0")] [Route("api/v{version:apiVersion}/[controller]")] -internal class PackageReferencesController : ControllerBase +internal class PackageReferencesController( + AppState appState, + AppStateManager appStateManager, + IExtensionHive extensionHive) : ControllerBase { // GET /api/packagereferences // POST /api/packagereferences // DELETE /api/packagereferences/{packageReferenceId} // GET /api/packagereferences/{packageReferenceId}/versions - private readonly AppState _appState; - private readonly AppStateManager _appStateManager; - private readonly IExtensionHive _extensionHive; - - public PackageReferencesController( - AppState appState, - AppStateManager appStateManager, - IExtensionHive extensionHive) - { - _appState = appState; - _appStateManager = appStateManager; - _extensionHive = extensionHive; - } + private readonly AppState _appState = appState; + private readonly AppStateManager _appStateManager = appStateManager; + private readonly IExtensionHive _extensionHive = extensionHive; /// /// Gets the list of package references. diff --git a/src/Nexus/API/SourcesController.cs b/src/Nexus/API/SourcesController.cs index 9f3954ec..5ccd9ff5 100644 --- a/src/Nexus/API/SourcesController.cs +++ b/src/Nexus/API/SourcesController.cs @@ -17,26 +17,19 @@ namespace Nexus.Controllers; [ApiController] [ApiVersion("1.0")] [Route("api/v{version:apiVersion}/[controller]")] -internal class SourcesController : ControllerBase +internal class SourcesController( + AppState appState, + AppStateManager appStateManager, + IExtensionHive extensionHive) : ControllerBase { // GET /api/sources/descriptions // GET /api/sources/registrations // POST /api/sources/registrations // DELETE /api/sources/registrations/{registrationId} - private readonly AppState _appState; - private readonly AppStateManager _appStateManager; - private readonly IExtensionHive _extensionHive; - - public SourcesController( - AppState appState, - AppStateManager appStateManager, - IExtensionHive extensionHive) - { - _appState = appState; - _appStateManager = appStateManager; - _extensionHive = extensionHive; - } + private readonly AppState _appState = appState; + private readonly AppStateManager _appStateManager = appStateManager; + private readonly IExtensionHive _extensionHive = extensionHive; /// /// Gets the list of source descriptions. diff --git a/src/Nexus/API/SystemController.cs b/src/Nexus/API/SystemController.cs index 064f37a7..151f3872 100644 --- a/src/Nexus/API/SystemController.cs +++ b/src/Nexus/API/SystemController.cs @@ -14,7 +14,10 @@ namespace Nexus.Controllers; [ApiController] [ApiVersion("1.0")] [Route("api/v{version:apiVersion}/[controller]")] -internal class SystemController : ControllerBase +internal class SystemController( + AppState appState, + AppStateManager appStateManager, + IOptions generalOptions) : ControllerBase { // [authenticated] // GET /api/system/configuration @@ -24,19 +27,9 @@ internal class SystemController : ControllerBase // [privileged] // PUT /api/system/configuration - private readonly AppState _appState; - private readonly AppStateManager _appStateManager; - private readonly GeneralOptions _generalOptions; - - public SystemController( - AppState appState, - AppStateManager appStateManager, - IOptions generalOptions) - { - _generalOptions = generalOptions.Value; - _appState = appState; - _appStateManager = appStateManager; - } + private readonly AppState _appState = appState; + private readonly AppStateManager _appStateManager = appStateManager; + private readonly GeneralOptions _generalOptions = generalOptions.Value; /// /// Gets the default file type. diff --git a/src/Nexus/API/UsersController.cs b/src/Nexus/API/UsersController.cs index 12b39cea..d27df9e9 100644 --- a/src/Nexus/API/UsersController.cs +++ b/src/Nexus/API/UsersController.cs @@ -22,7 +22,11 @@ namespace Nexus.Controllers; [ApiController] [ApiVersion("1.0")] [Route("api/v{version:apiVersion}/[controller]")] -internal class UsersController : ControllerBase +internal class UsersController( + IDBService dBService, + ITokenService tokenService, + IOptions securityOptions, + ILogger logger) : ControllerBase { // [anonymous] // GET /api/users/authenticate @@ -46,22 +50,10 @@ internal class UsersController : ControllerBase // GET /api/users/{userId}/tokens - private readonly IDBService _dbService; - private readonly ITokenService _tokenService; - private readonly SecurityOptions _securityOptions; - private readonly ILogger _logger; - - public UsersController( - IDBService dBService, - ITokenService tokenService, - IOptions securityOptions, - ILogger logger) - { - _dbService = dBService; - _tokenService = tokenService; - _securityOptions = securityOptions.Value; - _logger = logger; - } + private readonly IDBService _dbService = dBService; + private readonly ITokenService _tokenService = tokenService; + private readonly SecurityOptions _securityOptions = securityOptions.Value; + private readonly ILogger _logger = logger; #region Anonymous diff --git a/src/Nexus/API/WritersController.cs b/src/Nexus/API/WritersController.cs index 3c2aeb12..fe4e9dd4 100644 --- a/src/Nexus/API/WritersController.cs +++ b/src/Nexus/API/WritersController.cs @@ -11,17 +11,12 @@ namespace Nexus.Controllers; [ApiController] [ApiVersion("1.0")] [Route("api/v{version:apiVersion}/[controller]")] -internal class WritersController : ControllerBase +internal class WritersController( + AppState appState) : ControllerBase { // GET /api/writers/descriptions - private readonly AppState _appState; - - public WritersController( - AppState appState) - { - _appState = appState; - } + private readonly AppState _appState = appState; /// /// Gets the list of writer descriptions. diff --git a/src/Nexus/Core/CatalogContainer.cs b/src/Nexus/Core/CatalogContainer.cs index 1e342805..1b66682a 100644 --- a/src/Nexus/Core/CatalogContainer.cs +++ b/src/Nexus/Core/CatalogContainer.cs @@ -99,11 +99,8 @@ public async Task GetLazyCatalogInfoAsync(CancellationToken can { await EnsureLazyCatalogInfoAsync(cancellationToken); - var lazyCatalogInfo = _lazyCatalogInfo; - - if (lazyCatalogInfo is null) - throw new Exception("this should never happen"); - + var lazyCatalogInfo = _lazyCatalogInfo + ?? throw new Exception("this should never happen"); return lazyCatalogInfo; } finally diff --git a/src/Nexus/Core/CustomExtensions.cs b/src/Nexus/Core/CustomExtensions.cs index 10aef6de..d69fda53 100644 --- a/src/Nexus/Core/CustomExtensions.cs +++ b/src/Nexus/Core/CustomExtensions.cs @@ -33,8 +33,7 @@ internal static class CustomExtensions 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)); // + var hash = MD5.HashData(Encoding.UTF8.GetBytes(value)); return hash; } diff --git a/src/Nexus/Core/Models_Public.cs b/src/Nexus/Core/Models_Public.cs index b4c0c887..dbc2a958 100644 --- a/src/Nexus/Core/Models_Public.cs +++ b/src/Nexus/Core/Models_Public.cs @@ -8,35 +8,24 @@ namespace Nexus.Core; /// /// Represents a user. /// -public class NexusUser +public class NexusUser( + string id, + string name) { -#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 + public string Id { get; set; } = id; /// /// The user name. /// - public string Name { get; set; } = default!; + public string Name { get; set; } = name; #pragma warning disable CS1591 [JsonIgnore] - public List Claims { get; set; } = default!; + public List Claims { get; set; } = []; #pragma warning restore CS1591 @@ -45,32 +34,22 @@ public NexusUser( /// /// Represents a claim. /// -public class NexusClaim +public class NexusClaim(Guid id, string type, string value) { -#pragma warning disable CS1591 - - public NexusClaim(Guid id, string type, string value) - { - Id = id; - Type = type; - Value = value; - } - + /// [JsonIgnore] [ValidateNever] - public Guid Id { get; set; } - -#pragma warning restore CS1591 + public Guid Id { get; set; } = id; /// /// The claim type. /// - public string Type { get; init; } + public string Type { get; init; } = type; /// /// The claim value. /// - public string Value { get; init; } + public string Value { get; init; } = value; #pragma warning disable CS1591 diff --git a/src/Nexus/Core/NexusAuthExtensions.cs b/src/Nexus/Core/NexusAuthExtensions.cs index ec167506..11702a02 100644 --- a/src/Nexus/Core/NexusAuthExtensions.cs +++ b/src/Nexus/Core/NexusAuthExtensions.cs @@ -56,7 +56,7 @@ public static IServiceCollection AddNexusAuth( var providers = securityOptions.OidcProviders.Any() ? securityOptions.OidcProviders - : new List() { DefaultProvider }; + : [DefaultProvider]; foreach (var provider in providers) { @@ -131,11 +131,8 @@ public static IServiceCollection AddNexusAuth( // scopes // https://openid.net/specs/openid-connect-basic-1_0.html#Scopes - var principal = context.Principal; - - if (principal is null) - throw new Exception("The principal is null. This should never happen."); - + var principal = context.Principal + ?? throw new Exception("The principal is null. This should never happen."); var userId = principal.FindFirstValue(Claims.Subject) ?? throw new Exception("The subject claim is missing. This should never happen."); @@ -208,19 +205,15 @@ public static IServiceCollection AddNexusAuth( PersonalAccessTokenAuthenticationDefaults.AuthenticationScheme }; - services.AddAuthorization(options => - { - options.DefaultPolicy = new AuthorizationPolicyBuilder() + services.AddAuthorizationBuilder() + .SetDefaultPolicy(new AuthorizationPolicyBuilder() .RequireAuthenticatedUser() .RequireRole(NexusRoles.USER) .AddAuthenticationSchemes(authenticationSchemes) - .Build(); - - options - .AddPolicy(NexusPolicies.RequireAdmin, policy => policy + .Build()) + .AddPolicy(NexusPolicies.RequireAdmin, policy => policy .RequireRole(NexusRoles.ADMINISTRATOR) .AddAuthenticationSchemes(authenticationSchemes)); - }); return services; } diff --git a/src/Nexus/Core/NexusIdentityProviderExtensions.cs b/src/Nexus/Core/NexusIdentityProviderExtensions.cs index 24697d6d..6fe9f017 100644 --- a/src/Nexus/Core/NexusIdentityProviderExtensions.cs +++ b/src/Nexus/Core/NexusIdentityProviderExtensions.cs @@ -182,14 +182,9 @@ public static WebApplication UseNexusIdentityProvider( } } -internal class HostedService : IHostedService +internal class HostedService(IServiceProvider serviceProvider) : IHostedService { - private readonly IServiceProvider _serviceProvider; - - public HostedService(IServiceProvider serviceProvider) - { - _serviceProvider = serviceProvider; - } + private readonly IServiceProvider _serviceProvider = serviceProvider; public async Task StartAsync(CancellationToken cancellationToken) { diff --git a/src/Nexus/Core/NexusOptions.cs b/src/Nexus/Core/NexusOptions.cs index 8b52aad1..85d9ac21 100644 --- a/src/Nexus/Core/NexusOptions.cs +++ b/src/Nexus/Core/NexusOptions.cs @@ -100,5 +100,5 @@ 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 List OidcProviders { get; set; } = []; } \ No newline at end of file diff --git a/src/Nexus/Core/PersonalAccessTokenAuthenticationHandler.cs b/src/Nexus/Core/PersonalAccessTokenAuthenticationHandler.cs index c68b4e60..900811cc 100644 --- a/src/Nexus/Core/PersonalAccessTokenAuthenticationHandler.cs +++ b/src/Nexus/Core/PersonalAccessTokenAuthenticationHandler.cs @@ -13,22 +13,16 @@ internal static class PersonalAccessTokenAuthenticationDefaults public const string AuthenticationScheme = "pat"; } -internal class PersonalAccessTokenAuthHandler : AuthenticationHandler +internal class PersonalAccessTokenAuthHandler( + ITokenService tokenService, + IDBService dbService, + IOptionsMonitor options, + ILoggerFactory logger, + UrlEncoder encoder) : AuthenticationHandler(options, logger, encoder) { - private readonly ITokenService _tokenService; + private readonly ITokenService _tokenService = tokenService; - private readonly IDBService _dbService; - - public PersonalAccessTokenAuthHandler( - ITokenService tokenService, - IDBService dbService, - IOptionsMonitor options, - ILoggerFactory logger, - UrlEncoder encoder) : base(options, logger, encoder) - { - _tokenService = tokenService; - _dbService = dbService; - } + private readonly IDBService _dbService = dbService; protected async override Task HandleAuthenticateAsync() { @@ -79,8 +73,7 @@ protected async override Task HandleAuthenticateAsync() nameType: Claims.Name, roleType: Claims.Role); - if (principal is null) - principal = new ClaimsPrincipal(); + principal ??= new ClaimsPrincipal(); principal.AddIdentity(identity); } diff --git a/src/Nexus/Core/UserDbContext.cs b/src/Nexus/Core/UserDbContext.cs index 0c8f1fd3..49d6e6fa 100644 --- a/src/Nexus/Core/UserDbContext.cs +++ b/src/Nexus/Core/UserDbContext.cs @@ -2,14 +2,9 @@ namespace Nexus.Core; -internal class UserDbContext : DbContext +internal class UserDbContext(DbContextOptions options) + : DbContext(options) { - public UserDbContext(DbContextOptions options) - : base(options) - { - // - } - protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder diff --git a/src/Nexus/Extensibility/DataSource/DataSourceController.cs b/src/Nexus/Extensibility/DataSource/DataSourceController.cs index b5c86123..52991946 100644 --- a/src/Nexus/Extensibility/DataSource/DataSourceController.cs +++ b/src/Nexus/Extensibility/DataSource/DataSourceController.cs @@ -53,43 +53,30 @@ Task ReadAsync( CancellationToken cancellationToken); } -internal class DataSourceController : IDataSourceController +internal class DataSourceController( + IDataSource dataSource, + InternalDataSourceRegistration registration, + IReadOnlyDictionary? systemConfiguration, + IReadOnlyDictionary? requestConfiguration, + IProcessingService processingService, + ICacheService cacheService, + DataOptions dataOptions, + ILogger logger) : IDataSourceController { - private readonly IProcessingService _processingService; - private readonly ICacheService _cacheService; - private readonly DataOptions _dataOptions; + private readonly IProcessingService _processingService = processingService; + private readonly ICacheService _cacheService = cacheService; + private readonly DataOptions _dataOptions = dataOptions; private ConcurrentDictionary _catalogCache = default!; - 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; - } - - private IDataSource DataSource { get; } + private IDataSource DataSource { get; } = dataSource; - private InternalDataSourceRegistration DataSourceRegistration { get; } + private InternalDataSourceRegistration DataSourceRegistration { get; } = registration; - private IReadOnlyDictionary? SystemConfiguration { get; } + private IReadOnlyDictionary? SystemConfiguration { get; } = systemConfiguration; - internal IReadOnlyDictionary? RequestConfiguration { get; } + internal IReadOnlyDictionary? RequestConfiguration { get; } = requestConfiguration; - private ILogger Logger { get; } + private ILogger Logger { get; } = logger; public async Task InitializeAsync( ConcurrentDictionary catalogCache, @@ -239,7 +226,7 @@ public async Task GetCatalogAsync( .RepositoryUrl; var newResourceProperties = catalogProperties is null - ? new Dictionary() + ? [] : catalogProperties.ToDictionary(entry => entry.Key, entry => entry.Value); var originJsonObject = new JsonObject() @@ -564,7 +551,7 @@ private async Task ReadAggregatedAsync( if (disableCache) { - uncachedIntervals = new List { new Interval(begin, end) }; + uncachedIntervals = [new Interval(begin, end)]; } else @@ -607,7 +594,7 @@ private async Task ReadAggregatedAsync( await DataSource.ReadAsync( interval.Begin, interval.End, - new[] { slicedReadRequest }, + [slicedReadRequest], readDataHandler, progress, cancellationToken); @@ -721,7 +708,7 @@ private async Task ReadResampledAsync( await DataSource.ReadAsync( roundedBegin, roundedEnd, - new[] { readRequest }, + [readRequest], readDataHandler, progress, cancellationToken); @@ -779,7 +766,7 @@ private ReadUnit[] PrepareReadUnits( } } - return readUnits.ToArray(); + return [.. readUnits]; } public static async Task ReadAsync( diff --git a/src/Nexus/Extensibility/DataSource/DataSourceControllerExtensions.cs b/src/Nexus/Extensibility/DataSource/DataSourceControllerExtensions.cs index 5c42cb69..97649103 100644 --- a/src/Nexus/Extensibility/DataSource/DataSourceControllerExtensions.cs +++ b/src/Nexus/Extensibility/DataSource/DataSourceControllerExtensions.cs @@ -75,16 +75,16 @@ public static Task ReadSingleAsync( { var samplePeriod = request.Item.Representation.SamplePeriod; - var readingGroup = new DataReadingGroup(controller, new CatalogItemRequestPipeWriter[] - { + var readingGroup = new DataReadingGroup(controller, + [ new CatalogItemRequestPipeWriter(request, dataWriter) - }); + ]); return DataSourceController.ReadAsync( begin, end, samplePeriod, - new DataReadingGroup[] { readingGroup }, + [readingGroup], readDataHandler, memoryTracker, progress, diff --git a/src/Nexus/Extensibility/DataSource/DataSourceDoubleStream.cs b/src/Nexus/Extensibility/DataSource/DataSourceDoubleStream.cs index d9958d74..62fe7662 100644 --- a/src/Nexus/Extensibility/DataSource/DataSourceDoubleStream.cs +++ b/src/Nexus/Extensibility/DataSource/DataSourceDoubleStream.cs @@ -2,20 +2,14 @@ namespace Nexus.Extensibility; -internal class DataSourceDoubleStream : Stream +internal class DataSourceDoubleStream(long length, PipeReader reader) + : Stream { 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(); - } + private readonly long _length = length; + private readonly PipeReader _reader = reader; + private readonly Stream _stream = reader.AsStream(); public override bool CanRead => true; diff --git a/src/Nexus/Extensibility/DataWriter/DataWriterController.cs b/src/Nexus/Extensibility/DataWriter/DataWriterController.cs index 201e8f88..079a0e1c 100644 --- a/src/Nexus/Extensibility/DataWriter/DataWriterController.cs +++ b/src/Nexus/Extensibility/DataWriter/DataWriterController.cs @@ -24,31 +24,22 @@ Task WriteAsync( // TODO: Add "CheckFileSize" method (e.g. for Famos). -internal class DataWriterController : IDataWriterController +internal class DataWriterController( + IDataWriter dataWriter, + Uri resourceLocator, + IReadOnlyDictionary? systemConfiguration, + IReadOnlyDictionary? requestConfiguration, + ILogger logger) : IDataWriterController { - public DataWriterController( - IDataWriter dataWriter, - Uri resourceLocator, - IReadOnlyDictionary? systemConfiguration, - IReadOnlyDictionary? requestConfiguration, - ILogger logger) - { - DataWriter = dataWriter; - ResourceLocator = resourceLocator; - SystemConfiguration = systemConfiguration; - RequestConfiguration = requestConfiguration; - Logger = logger; - } - - private IReadOnlyDictionary? SystemConfiguration { get; } + private IReadOnlyDictionary? SystemConfiguration { get; } = systemConfiguration; - private IReadOnlyDictionary? RequestConfiguration { get; } + private IReadOnlyDictionary? RequestConfiguration { get; } = requestConfiguration; - private IDataWriter DataWriter { get; } + private IDataWriter DataWriter { get; } = dataWriter; - private Uri ResourceLocator { get; } + private Uri ResourceLocator { get; } = resourceLocator; - private ILogger Logger { get; } + private ILogger Logger { get; } = logger; public async Task InitializeAsync( ILogger logger, diff --git a/src/Nexus/Extensions/Sources/Sample.cs b/src/Nexus/Extensions/Sources/Sample.cs index 93624630..e2fdc776 100644 --- a/src/Nexus/Extensions/Sources/Sample.cs +++ b/src/Nexus/Extensions/Sources/Sample.cs @@ -103,8 +103,8 @@ public Task GetCatalogRegistrationsAsync( if (path == "/") return Task.FromResult(new CatalogRegistration[] { - new CatalogRegistration(LocalCatalogId, LocalCatalogTitle), - new CatalogRegistration(RemoteCatalogId, RemoteCatalogTitle), + new(LocalCatalogId, LocalCatalogTitle), + new(RemoteCatalogId, RemoteCatalogTitle), }); else @@ -200,7 +200,7 @@ public async Task ReadAsync( var finishedTasks = 0; - while (tasks.Any()) + while (tasks.Count != 0) { var task = await Task.WhenAny(tasks); cancellationToken.ThrowIfCancellationRequested(); diff --git a/src/Nexus/Extensions/Writers/Csv.cs b/src/Nexus/Extensions/Writers/Csv.cs index 702e6ec9..efc4a2fd 100644 --- a/src/Nexus/Extensions/Writers/Csv.cs +++ b/src/Nexus/Extensions/Writers/Csv.cs @@ -69,7 +69,7 @@ internal class Csv : IDataWriter, IDisposable private double _excelStart; private DateTime _lastFileBegin; private TimeSpan _lastSamplePeriod; - private readonly Dictionary _resourceMap = new(); + private readonly Dictionary _resourceMap = []; private DataWriterContext Context { get; set; } = default!; @@ -123,7 +123,7 @@ public async Task OpenAsync( var layout = new Layout() { - HeaderRows = new[] { 4 } + HeaderRows = [4] }; var fields = new[] { timestampField }.Concat(catalogItemGroup.Select(catalogItem => @@ -150,7 +150,7 @@ public async Task OpenAsync( Name: resourceFileNameWithoutExtension.ToLower(), Profile: "tabular-data-resource", Scheme: "multipart", - Path: new List(), + Path: [], Layout: layout, Schema: schema); diff --git a/src/Nexus/PackageManagement/PackageController.cs b/src/Nexus/PackageManagement/PackageController.cs index cb550618..fe55505f 100644 --- a/src/Nexus/PackageManagement/PackageController.cs +++ b/src/Nexus/PackageManagement/PackageController.cs @@ -12,7 +12,9 @@ namespace Nexus.PackageManagement; -internal partial class PackageController +internal partial class PackageController( + InternalPackageReference packageReference, + ILogger logger) { public static Guid BUILTIN_ID = new("97d297d2-df6f-4c85-9d07-86bc64a041a6"); public const string BUILTIN_PROVIDER = "nexus"; @@ -22,16 +24,10 @@ internal partial class PackageController private static readonly HttpClient _httpClient = new(); - private readonly ILogger _logger; + private readonly ILogger _logger = logger; private PackageLoadContext? _loadContext; - public PackageController(InternalPackageReference packageReference, ILogger logger) - { - PackageReference = packageReference; - _logger = logger; - } - - public InternalPackageReference PackageReference { get; } + public InternalPackageReference PackageReference { get; } = packageReference; public async Task DiscoverAsync(CancellationToken cancellationToken) { @@ -73,16 +69,8 @@ public async Task LoadAsync(string restoreRoot, CancellationToken canc var depsJsonFilePath = Directory .EnumerateFiles(restoreFolderPath, $"*{depsJsonExtension}", SearchOption.AllDirectories) - .SingleOrDefault(); - - if (depsJsonFilePath is null) - throw new Exception($"Could not determine the location of the .deps.json file in folder {restoreFolderPath}."); - - var entryDllPath = depsJsonFilePath[..^depsJsonExtension.Length] + ".dll"; - - if (entryDllPath is null) - throw new Exception($"Could not determine the location of the entry DLL file in folder {restoreFolderPath}."); - + .SingleOrDefault() ?? throw new Exception($"Could not determine the location of the .deps.json file in folder {restoreFolderPath}."); + var entryDllPath = depsJsonFilePath[..^depsJsonExtension.Length] + ".dll" ?? throw new Exception($"Could not determine the location of the entry DLL file in folder {restoreFolderPath}."); _loadContext = new PackageLoadContext(entryDllPath); var assemblyName = new AssemblyName(Path.GetFileNameWithoutExtension(entryDllPath)); @@ -280,11 +268,7 @@ private async Task DiscoverGitTagsAsync(CancellationToken cancellation RedirectStandardError = true }; - using var process = Process.Start(startInfo); - - if (process is null) - throw new Exception("Process is null."); - + using var process = Process.Start(startInfo) ?? throw new Exception("Process is null."); while (!process.StandardOutput.EndOfStream) { var refLine = await process.StandardOutput.ReadLineAsync(cancellationToken); @@ -326,7 +310,7 @@ private async Task DiscoverGitTagsAsync(CancellationToken cancellation result.Reverse(); - return result.ToArray(); + return [.. result]; } private async Task RestoreGitTagAsync(string restoreRoot, CancellationToken cancellationToken) @@ -365,11 +349,7 @@ private async Task RestoreGitTagAsync(string restoreRoot, CancellationTo RedirectStandardError = true }; - using var process1 = Process.Start(startInfo1); - - if (process1 is null) - throw new Exception("Process is null."); - + using var process1 = Process.Start(startInfo1) ?? throw new Exception("Process is null."); await process1.WaitForExitAsync(cancellationToken); if (process1.ExitCode != 0) @@ -400,11 +380,7 @@ private async Task RestoreGitTagAsync(string restoreRoot, CancellationTo RedirectStandardError = true }; - using var process2 = Process.Start(startInfo2); - - if (process2 is null) - throw new Exception("Process is null."); - + using var process2 = Process.Start(startInfo2) ?? throw new Exception("Process is null."); await process2.WaitForExitAsync(cancellationToken); if (process2.ExitCode != 0) @@ -518,7 +494,7 @@ private async Task DiscoverGithubReleasesAsync(CancellationToken cance continue; } - return result.ToArray(); + return [.. result]; } private async Task RestoreGitHubReleasesAsync(string restoreRoot, CancellationToken cancellationToken) @@ -633,7 +609,7 @@ private async Task DiscoverGitLabPackagesGenericAsync(CancellationToke result.Reverse(); - return result.ToArray(); + return [.. result]; } private async Task RestoreGitLabPackagesGenericAsync(string restoreRoot, CancellationToken cancellationToken) diff --git a/src/Nexus/PackageManagement/PackageLoadContext.cs b/src/Nexus/PackageManagement/PackageLoadContext.cs index ec18b297..dd4720c4 100644 --- a/src/Nexus/PackageManagement/PackageLoadContext.cs +++ b/src/Nexus/PackageManagement/PackageLoadContext.cs @@ -3,14 +3,10 @@ namespace Nexus.PackageManagement; -internal class PackageLoadContext : AssemblyLoadContext +internal class PackageLoadContext(string entryDllPath) + : AssemblyLoadContext(isCollectible: true) { - private readonly AssemblyDependencyResolver _resolver; - - public PackageLoadContext(string entryDllPath) : base(isCollectible: true) - { - _resolver = new AssemblyDependencyResolver(entryDllPath); - } + private readonly AssemblyDependencyResolver _resolver = new(entryDllPath); protected override Assembly? Load(AssemblyName assemblyName) { diff --git a/src/Nexus/Program.cs b/src/Nexus/Program.cs index 5709ad20..82f0180e 100644 --- a/src/Nexus/Program.cs +++ b/src/Nexus/Program.cs @@ -238,7 +238,7 @@ void ConfigurePipeline(WebApplication app) app.UseAuthorization(); // endpoints - + /* REST API */ app.MapControllers(); diff --git a/src/Nexus/Services/AppStateManager.cs b/src/Nexus/Services/AppStateManager.cs index 2823a0f5..d83f0847 100644 --- a/src/Nexus/Services/AppStateManager.cs +++ b/src/Nexus/Services/AppStateManager.cs @@ -7,30 +7,21 @@ namespace Nexus.Services; -internal class AppStateManager +internal class AppStateManager( + AppState appState, + IExtensionHive extensionHive, + ICatalogManager catalogManager, + IDatabaseService databaseService, + ILogger logger) { - private readonly IExtensionHive _extensionHive; - private readonly ICatalogManager _catalogManager; - private readonly IDatabaseService _databaseService; - private readonly ILogger _logger; + private readonly IExtensionHive _extensionHive = extensionHive; + private readonly ICatalogManager _catalogManager = catalogManager; + private readonly IDatabaseService _databaseService = databaseService; + private readonly ILogger _logger = logger; private readonly SemaphoreSlim _refreshDatabaseSemaphore = new(initialCount: 1, maxCount: 1); private readonly SemaphoreSlim _projectSemaphore = new(initialCount: 1, maxCount: 1); - public AppStateManager( - AppState appState, - IExtensionHive extensionHive, - ICatalogManager catalogManager, - IDatabaseService databaseService, - ILogger logger) - { - AppState = appState; - _extensionHive = extensionHive; - _catalogManager = catalogManager; - _databaseService = databaseService; - _logger = logger; - } - - public AppState AppState { get; } + public AppState AppState { get; } = appState; public async Task RefreshDatabaseAsync( IProgress progress, @@ -250,11 +241,7 @@ private void LoadDataWriters() } 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 label = (additionalInformation?.GetStringValue(Nexus.UI.Core.Constants.DATA_WRITER_LABEL_KEY)) ?? throw new Exception($"The description of data writer {fullName} has no label property"); var version = dataWriterType.Assembly .GetCustomAttribute()! .InformationalVersion; diff --git a/src/Nexus/Services/CacheService.cs b/src/Nexus/Services/CacheService.cs index 12464a50..16d8e748 100644 --- a/src/Nexus/Services/CacheService.cs +++ b/src/Nexus/Services/CacheService.cs @@ -28,17 +28,12 @@ Task ClearAsync( CancellationToken cancellationToken); } -internal class CacheService : ICacheService +internal class CacheService( + IDatabaseService databaseService) : ICacheService { - private readonly IDatabaseService _databaseService; + private readonly IDatabaseService _databaseService = databaseService; private readonly TimeSpan _largestSamplePeriod = TimeSpan.FromDays(1); - public CacheService( - IDatabaseService databaseService) - { - _databaseService = databaseService; - } - public async Task> ReadAsync( CatalogItem catalogItem, DateTime begin, diff --git a/src/Nexus/Services/CatalogManager.cs b/src/Nexus/Services/CatalogManager.cs index f4031714..44e4feb3 100644 --- a/src/Nexus/Services/CatalogManager.cs +++ b/src/Nexus/Services/CatalogManager.cs @@ -16,7 +16,13 @@ Task GetCatalogContainersAsync( CancellationToken cancellationToken); } -internal class CatalogManager : ICatalogManager +internal class CatalogManager( + AppState appState, + IDataControllerService dataControllerService, + IDatabaseService databaseService, + IServiceProvider serviceProvider, + IExtensionHive extensionHive, + ILogger logger) : ICatalogManager { record CatalogPrototype( CatalogRegistration Registration, @@ -25,28 +31,12 @@ record CatalogPrototype( CatalogMetadata Metadata, ClaimsPrincipal? Owner); - private readonly AppState _appState; - private readonly IDataControllerService _dataControllerService; - private readonly IDatabaseService _databaseService; - private readonly IServiceProvider _serviceProvider; - private readonly IExtensionHive _extensionHive; - private readonly ILogger _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; - } + private readonly AppState _appState = appState; + private readonly IDataControllerService _dataControllerService = dataControllerService; + private readonly IDatabaseService _databaseService = databaseService; + private readonly IServiceProvider _serviceProvider = serviceProvider; + private readonly IExtensionHive _extensionHive = extensionHive; + private readonly ILogger _logger = logger; public async Task GetCatalogContainersAsync( CatalogContainer parent, @@ -65,7 +55,7 @@ public async Task GetCatalogContainersAsync( /* load builtin data source */ var builtinDataSourceRegistrations = new InternalDataSourceRegistration[] { - new InternalDataSourceRegistration( + new( Id: Sample.RegistrationId, Type: typeof(Sample).FullName!, ResourceLocator: default, @@ -195,7 +185,7 @@ public async Task GetCatalogContainersAsync( catch (Exception ex) { _logger.LogWarning(ex, "Unable to get or process child data source registrations"); - catalogContainers = Array.Empty(); + catalogContainers = []; } } @@ -301,6 +291,6 @@ private CatalogPrototype[] EnsureNoHierarchy( } } - return catalogPrototypesToKeep.ToArray(); + return [.. catalogPrototypesToKeep]; } } diff --git a/src/Nexus/Services/DataControllerService.cs b/src/Nexus/Services/DataControllerService.cs index 36eeb7ec..7d9c7d6e 100644 --- a/src/Nexus/Services/DataControllerService.cs +++ b/src/Nexus/Services/DataControllerService.cs @@ -19,38 +19,26 @@ Task GetDataWriterControllerAsync( CancellationToken cancellationToken); } -internal class DataControllerService : IDataControllerService +internal class DataControllerService( + AppState appState, + IHttpContextAccessor httpContextAccessor, + IExtensionHive extensionHive, + IProcessingService processingService, + ICacheService cacheService, + IOptions dataOptions, + ILogger logger, + ILoggerFactory loggerFactory) : 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) - { - _appState = appState; - _httpContextAccessor = httpContextAccessor; - _extensionHive = extensionHive; - _processingService = processingService; - _cacheService = cacheService; - _dataOptions = dataOptions.Value; - _logger = logger; - _loggerFactory = loggerFactory; - } + private readonly AppState _appState = appState; + private readonly DataOptions _dataOptions = dataOptions.Value; + private readonly IHttpContextAccessor _httpContextAccessor = httpContextAccessor; + private readonly IExtensionHive _extensionHive = extensionHive; + private readonly IProcessingService _processingService = processingService; + private readonly ICacheService _cacheService = cacheService; + private readonly ILogger _logger = logger; + private readonly ILoggerFactory _loggerFactory = loggerFactory; public async Task GetDataSourceControllerAsync( InternalDataSourceRegistration registration, diff --git a/src/Nexus/Services/DataService.cs b/src/Nexus/Services/DataService.cs index e76ede98..eee4c8dd 100644 --- a/src/Nexus/Services/DataService.cs +++ b/src/Nexus/Services/DataService.cs @@ -34,40 +34,26 @@ Task ExportAsync( CancellationToken cancellationToken); } -internal class DataService : IDataService +internal class DataService( + AppState appState, + ClaimsPrincipal user, + IDataControllerService dataControllerService, + IDatabaseService databaseService, + IMemoryTracker memoryTracker, + ILogger logger, + ILoggerFactory loggerFactory) : IDataService { - 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; - - 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(); - } + private readonly AppState _appState = appState; + private readonly IMemoryTracker _memoryTracker = memoryTracker; + private readonly ClaimsPrincipal _user = user; + private readonly ILogger _logger = logger; + private readonly ILoggerFactory _loggerFactory = loggerFactory; + private readonly IDatabaseService _databaseService = databaseService; + private readonly IDataControllerService _dataControllerService = dataControllerService; - public Progress ReadProgress { get; } + public Progress ReadProgress { get; } = new Progress(); - public Progress WriteProgress { get; } + public Progress WriteProgress { get; } = new Progress(); public async Task ReadAsStreamAsync( string resourcePath, @@ -80,11 +66,7 @@ public async Task ReadAsStreamAsync( // 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 catalogItemRequest = await root.TryFindAsync(resourcePath, cancellationToken) ?? throw new Exception($"Could not find resource path {resourcePath}."); var catalogContainer = catalogItemRequest.Container; // security check diff --git a/src/Nexus/Services/DatabaseService.cs b/src/Nexus/Services/DatabaseService.cs index 53ba88dd..e18ab825 100644 --- a/src/Nexus/Services/DatabaseService.cs +++ b/src/Nexus/Services/DatabaseService.cs @@ -41,7 +41,8 @@ Stream WriteTokenMap( string userId); } -internal class DatabaseService : IDatabaseService +internal class DatabaseService(IOptions pathsOptions) + : IDatabaseService { // generated, small files: // @@ -57,12 +58,7 @@ internal class DatabaseService : IDatabaseService // /export // /.nexus/packages - private readonly PathsOptions _pathsOptions; - - public DatabaseService(IOptions pathsOptions) - { - _pathsOptions = pathsOptions.Value; - } + private readonly PathsOptions _pathsOptions = pathsOptions.Value; /* /config/catalogs/catalog_id.json */ public bool TryReadCatalogMetadata(string catalogId, [NotNullWhen(true)] out string? catalogMetadata) diff --git a/src/Nexus/Services/DbService.cs b/src/Nexus/Services/DbService.cs index 61007328..3ab8645a 100644 --- a/src/Nexus/Services/DbService.cs +++ b/src/Nexus/Services/DbService.cs @@ -14,15 +14,10 @@ internal interface IDBService Task SaveChangesAsync(); } -internal class DbService : IDBService +internal class DbService( + UserDbContext context) : IDBService { - private readonly UserDbContext _context; - - public DbService( - UserDbContext context) - { - _context = context; - } + private readonly UserDbContext _context = context; public IQueryable GetUsers() { diff --git a/src/Nexus/Services/ExtensionHive.cs b/src/Nexus/Services/ExtensionHive.cs index 0147fcdd..42cf0253 100644 --- a/src/Nexus/Services/ExtensionHive.cs +++ b/src/Nexus/Services/ExtensionHive.cs @@ -29,24 +29,17 @@ Task GetVersionsAsync( CancellationToken cancellationToken); } -internal class ExtensionHive : IExtensionHive +internal class ExtensionHive( + IOptions pathsOptions, + ILogger logger, + ILoggerFactory loggerFactory) : IExtensionHive { - private readonly ILogger _logger; - private readonly ILoggerFactory _loggerFactory; - private readonly PathsOptions _pathsOptions; + private readonly ILogger _logger = logger; + private readonly ILoggerFactory _loggerFactory = loggerFactory; + private readonly PathsOptions _pathsOptions = pathsOptions.Value; private Dictionary>? _packageControllerMap = default!; - public ExtensionHive( - IOptions pathsOptions, - ILogger logger, - ILoggerFactory loggerFactory) - { - _logger = logger; - _loggerFactory = loggerFactory; - _pathsOptions = pathsOptions.Value; - } - public async Task LoadPackagesAsync( IEnumerable packageReferences, IProgress progress, @@ -68,7 +61,7 @@ public async Task LoadPackagesAsync( var nexusPackageReference = new InternalPackageReference( Id: PackageController.BUILTIN_ID, Provider: PackageController.BUILTIN_PROVIDER, - Configuration: new Dictionary() + Configuration: [] ); packageReferences = new List() { nexusPackageReference }.Concat(packageReferences); diff --git a/src/Nexus/Services/MemoryTracker.cs b/src/Nexus/Services/MemoryTracker.cs index 8339d4ec..7224d5c6 100644 --- a/src/Nexus/Services/MemoryTracker.cs +++ b/src/Nexus/Services/MemoryTracker.cs @@ -3,18 +3,14 @@ namespace Nexus.Services; -internal class AllocationRegistration : IDisposable +internal class AllocationRegistration( + IMemoryTracker tracker, + long actualByteCount) : IDisposable { private bool _disposedValue; - private readonly IMemoryTracker _tracker; + private readonly IMemoryTracker _tracker = tracker; - public AllocationRegistration(IMemoryTracker tracker, long actualByteCount) - { - _tracker = tracker; - ActualByteCount = actualByteCount; - } - - public long ActualByteCount { get; } + public long ActualByteCount { get; } = actualByteCount; public void Dispose() { @@ -36,7 +32,7 @@ internal class MemoryTracker : IMemoryTracker { private long _consumedBytes; private readonly DataOptions _dataOptions; - private readonly List _retrySemaphores = new(); + private readonly List _retrySemaphores = []; private readonly ILogger _logger; public MemoryTracker(IOptions dataOptions, ILogger logger) diff --git a/src/Nexus/Services/ProcessingService.cs b/src/Nexus/Services/ProcessingService.cs index 54ccf4c8..8488b705 100644 --- a/src/Nexus/Services/ProcessingService.cs +++ b/src/Nexus/Services/ProcessingService.cs @@ -28,14 +28,10 @@ void Aggregate( int blockSize); } -internal class ProcessingService : IProcessingService +internal class ProcessingService(IOptions dataOptions) + : IProcessingService { - private readonly double _nanThreshold; - - public ProcessingService(IOptions dataOptions) - { - _nanThreshold = dataOptions.Value.AggregationNaNThreshold; - } + private readonly double _nanThreshold = dataOptions.Value.AggregationNaNThreshold; public void Resample( NexusDataType dataType, @@ -46,7 +42,7 @@ public void Resample( int offset) { using var memoryOwner = MemoryPool.Shared.Rent(status.Length); - var doubleData = memoryOwner.Memory.Slice(0, status.Length); + var doubleData = memoryOwner.Memory[..status.Length]; BufferUtilities.ApplyRepresentationStatusByDataType( dataType, @@ -103,7 +99,7 @@ private void GenericProcess( using (var memoryOwner = MemoryPool.Shared.Rent(Tdata.Length)) { - var doubleData2 = memoryOwner.Memory.Slice(0, Tdata.Length); + var doubleData2 = memoryOwner.Memory[..Tdata.Length]; BufferUtilities.ApplyRepresentationStatus(Tdata, status, target: doubleData2); ApplyAggregationFunction(kind, blockSize, doubleData2, targetBuffer); @@ -152,8 +148,8 @@ private void ApplyAggregationFunction( using (var memoryOwner_sin = MemoryPool.Shared.Rent(targetBuffer.Length)) using (var memoryOwner_cos = MemoryPool.Shared.Rent(targetBuffer.Length)) { - var sinBuffer = memoryOwner_sin.Memory.Slice(0, targetBuffer.Length); - var cosBuffer = memoryOwner_cos.Memory.Slice(0, targetBuffer.Length); + var sinBuffer = memoryOwner_sin.Memory[..targetBuffer.Length]; + var cosBuffer = memoryOwner_cos.Memory[..targetBuffer.Length]; var limit = 360; var factor = 2 * Math.PI / limit; diff --git a/src/Nexus/Services/TokenService.cs b/src/Nexus/Services/TokenService.cs index 343bc0ac..21195cf2 100644 --- a/src/Nexus/Services/TokenService.cs +++ b/src/Nexus/Services/TokenService.cs @@ -27,20 +27,16 @@ bool TryGet( Task> GetAllAsync(string userId); } -internal class TokenService : ITokenService +internal class TokenService(IDatabaseService databaseService) + : ITokenService { private static readonly RandomNumberGenerator _rng = RandomNumberGenerator.Create(); - private readonly SemaphoreSlim _semaphoreSlim = new SemaphoreSlim(1, 1); + private readonly SemaphoreSlim _semaphoreSlim = new(1, 1); private readonly ConcurrentDictionary> _cache = new(); - private readonly IDatabaseService _databaseService; - - public TokenService(IDatabaseService databaseService) - { - _databaseService = databaseService; - } + private readonly IDatabaseService _databaseService = databaseService; public Task CreateAsync( string userId, diff --git a/src/Nexus/Utilities/MemoryManager.cs b/src/Nexus/Utilities/MemoryManager.cs index af2eab65..cad24e75 100644 --- a/src/Nexus/Utilities/MemoryManager.cs +++ b/src/Nexus/Utilities/MemoryManager.cs @@ -5,13 +5,11 @@ namespace Nexus.Utilities; // TODO: Validate against this: https://github.com/windows-toolkit/WindowsCommunityToolkit/pull/3520/files -internal class CastMemoryManager : MemoryManager +internal class CastMemoryManager(Memory from) : MemoryManager where TFrom : struct where TTo : struct { - private readonly Memory _from; - - public CastMemoryManager(Memory from) => _from = from; + private readonly Memory _from = from; public override Span GetSpan() => MemoryMarshal.Cast(_from.Span); diff --git a/src/Nexus/Utilities/NexusUtilities.cs b/src/Nexus/Utilities/NexusUtilities.cs index 729de5ea..561388d3 100644 --- a/src/Nexus/Utilities/NexusUtilities.cs +++ b/src/Nexus/Utilities/NexusUtilities.cs @@ -6,7 +6,7 @@ namespace Nexus.Utilities; -internal static class NexusUtilities +internal static partial class NexusUtilities { private static string? _defaultBaseUrl; @@ -21,7 +21,7 @@ public static string DefaultBaseUrl if (aspnetcoreEnvVar is not null) { - var match = Regex.Match(aspnetcoreEnvVar, ":([0-9]+)"); + var match = AspNetCoreEnvVarRegex().Match(aspnetcoreEnvVar); if (match.Success && int.TryParse(match.Groups[1].Value, out var parsedPort)) port = parsedPort; @@ -105,7 +105,7 @@ public static async ValueTask WhenAll(params ValueTask[] tasks) public static async Task WhenAllFailFastAsync(List tasks, CancellationToken cancellationToken) { - while (tasks.Any()) + while (tasks.Count != 0) { var task = await Task .WhenAny(tasks) @@ -148,4 +148,7 @@ public static IEnumerable GetCustomAttributes(this Type type) where T : At { return type.GetCustomAttributes(false).OfType(); } + + [GeneratedRegex(":([0-9]+)")] + private static partial Regex AspNetCoreEnvVarRegex(); } \ 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 c5338556..6192dd9f 100644 --- a/src/extensibility/dotnet-extensibility/DataModel/DataModelExtensions.cs +++ b/src/extensibility/dotnet-extensibility/DataModel/DataModelExtensions.cs @@ -6,7 +6,7 @@ namespace Nexus.DataModel; /// /// Contains extension methods to make life easier working with the data model types. /// -public static class DataModelExtensions +public static partial class DataModelExtensions { #region Fluent API @@ -133,11 +133,11 @@ public static string ToPath(this Uri url) // 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" }; + private static readonly long[] _nanoseconds = [(long)1e0, (long)1e3, (long)1e6, (long)1e9, (long)60e9, (long)3600e9, (long)86400e9]; + private static readonly int[] _quotients = [1000, 1000, 1000, 60, 60, 24, 1]; + private static readonly string[] _postFixes = ["ns", "us", "ms", "s", "min", "h", "d"]; // ... except this line - private static readonly Regex _unitStringEvaluator = new(@"^([0-9]+)_([a-z]+)$", RegexOptions.Compiled); + private static readonly Regex _unitStringEvaluator = UnitStringEvaluator(); /// /// Converts period into a human readable number string with unit. @@ -185,5 +185,8 @@ internal static TimeSpan ToSamplePeriod(string unitString) return new TimeSpan(ticks); } + [GeneratedRegex(@"^([0-9]+)_([a-z]+)$", RegexOptions.Compiled)] + private static partial Regex UnitStringEvaluator(); + #endregion } diff --git a/src/extensibility/dotnet-extensibility/DataModel/DataModelUtilities.cs b/src/extensibility/dotnet-extensibility/DataModel/DataModelUtilities.cs index bbcf8de7..996c5562 100644 --- a/src/extensibility/dotnet-extensibility/DataModel/DataModelUtilities.cs +++ b/src/extensibility/dotnet-extensibility/DataModel/DataModelUtilities.cs @@ -5,7 +5,7 @@ namespace Nexus.DataModel; -internal static class DataModelUtilities +internal static partial class DataModelUtilities { /* Example resource paths: * @@ -18,7 +18,7 @@ internal static class DataModelUtilities * /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 readonly Regex _resourcePathEvaluator = ResourcePathEvaluator(); private static string ToPascalCase(string input) { @@ -285,4 +285,7 @@ private static void MergeArrays(JsonArray currentArray, JsonElement root1, JsonE _ => JsonValue.Create(element) }; } + + [GeneratedRegex(@"^(?'catalog'.*)\/(?'resource'.*)\/(?'sample_period'[0-9]+_[a-zA-Z]+)(?:_(?'kind'[^\(#\s]+))?(?:\((?'parameters'.*)\))?(?:#(?'fragment'.*))?$", RegexOptions.Compiled)] + private static partial Regex ResourcePathEvaluator(); } \ 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 7dd6ebfa..48a63b67 100644 --- a/src/extensibility/dotnet-extensibility/DataModel/Representation.cs +++ b/src/extensibility/dotnet-extensibility/DataModel/Representation.cs @@ -9,9 +9,9 @@ namespace Nexus.DataModel; /// A representation is part of a resource. /// [DebuggerDisplay("{Id,nq}")] -public record Representation +public partial record Representation { - private static readonly Regex _snakeCaseEvaluator = new("(?<=[a-z])([A-Z])", RegexOptions.Compiled); + private static readonly Regex _snakeCaseEvaluator = SnakeCaseEvaluator(); private static readonly HashSet _nexusDataTypeValues = new(Enum.GetValues()); private IReadOnlyDictionary? _parameters; @@ -136,4 +136,7 @@ private static void ValidateParameters(IReadOnlyDictionary throw new Exception("The representation argument identifier is not valid."); } } + + [GeneratedRegex("(?<=[a-z])([A-Z])", RegexOptions.Compiled)] + private static partial Regex SnakeCaseEvaluator(); } \ 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 29bf620a..eec1667c 100644 --- a/src/extensibility/dotnet-extensibility/DataModel/Resource.cs +++ b/src/extensibility/dotnet-extensibility/DataModel/Resource.cs @@ -9,7 +9,7 @@ namespace Nexus.DataModel; /// A resource is part of a resource catalog and holds a list of representations. /// [DebuggerDisplay("{Id,nq}")] -public record Resource +public partial record Resource { private string _id = default!; private IReadOnlyList? _representations; @@ -36,7 +36,7 @@ public Resource( /// [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; } = ValidExpression(); /// /// Gets a regular expression to find invalid characters in a resource identifier. @@ -129,4 +129,7 @@ private static void ValidateRepresentations(IReadOnlyList repres if (uniqueIds.Count() != representations.Count) throw new ArgumentException("There are multiple representations with the same identifier."); } + + [GeneratedRegex(@"^[a-zA-Z_][a-zA-Z_0-9]*$")] + private static partial Regex ValidExpression(); } \ 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 ac1fdaa4..24f8b5cd 100644 --- a/src/extensibility/dotnet-extensibility/DataModel/ResourceBuilder.cs +++ b/src/extensibility/dotnet-extensibility/DataModel/ResourceBuilder.cs @@ -30,7 +30,7 @@ public ResourceBuilder(string id) /// The resource builder. public ResourceBuilder WithProperty(string key, object value) { - _properties ??= new(); + _properties ??= []; _properties[key] = JsonSerializer.SerializeToElement(value); @@ -44,7 +44,7 @@ public ResourceBuilder WithProperty(string key, object value) /// The resource builder. public ResourceBuilder AddRepresentation(Representation representation) { - _representations ??= new List(); + _representations ??= []; _representations.Add(representation); @@ -68,7 +68,7 @@ public ResourceBuilder AddRepresentations(params Representation[] representation /// The resource builder. public ResourceBuilder AddRepresentations(IEnumerable representations) { - _representations ??= new List(); + _representations ??= []; _representations.AddRange(representations); diff --git a/src/extensibility/dotnet-extensibility/DataModel/ResourceCatalog.cs b/src/extensibility/dotnet-extensibility/DataModel/ResourceCatalog.cs index 43d6824d..8e50d701 100644 --- a/src/extensibility/dotnet-extensibility/DataModel/ResourceCatalog.cs +++ b/src/extensibility/dotnet-extensibility/DataModel/ResourceCatalog.cs @@ -11,7 +11,7 @@ namespace Nexus.DataModel; /// A catalog is a top level element and holds a list of resources. /// [DebuggerDisplay("{Id,nq}")] -public record ResourceCatalog +public partial record ResourceCatalog { private string _id = default!; private IReadOnlyList? _resources; @@ -38,7 +38,7 @@ public ResourceCatalog( /// [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; } = ValidIdExpressionRegex(); [JsonIgnore] #warning Remove this when https://github.com/RicoSuter/NSwag/issues/4681 is solved @@ -202,4 +202,7 @@ private static void ValidateResources(IReadOnlyList resources) if (uniqueIds.Count() != resources.Count) throw new ArgumentException("There are multiple resources with the same identifier."); } + + [GeneratedRegex(@"^(?:\/[a-zA-Z_][a-zA-Z_0-9]*)+$", RegexOptions.Compiled)] + private static partial Regex ValidIdExpressionRegex(); } diff --git a/src/extensibility/dotnet-extensibility/DataModel/ResourceCatalogBuilder.cs b/src/extensibility/dotnet-extensibility/DataModel/ResourceCatalogBuilder.cs index 0a7b588c..f22f567a 100644 --- a/src/extensibility/dotnet-extensibility/DataModel/ResourceCatalogBuilder.cs +++ b/src/extensibility/dotnet-extensibility/DataModel/ResourceCatalogBuilder.cs @@ -28,7 +28,7 @@ public ResourceCatalogBuilder(string id) /// The resource catalog builder. public ResourceCatalogBuilder WithProperty(string key, JsonElement value) { - _properties ??= new(); + _properties ??= []; _properties[key] = value; @@ -43,7 +43,7 @@ public ResourceCatalogBuilder WithProperty(string key, JsonElement value) /// The resource catalog builder. public ResourceCatalogBuilder WithProperty(string key, object value) { - _properties ??= new(); + _properties ??= []; _properties[key] = JsonSerializer.SerializeToElement(value); @@ -57,7 +57,7 @@ public ResourceCatalogBuilder WithProperty(string key, object value) /// The resource catalog builder. public ResourceCatalogBuilder AddResource(Resource resource) { - _resources ??= new List(); + _resources ??= []; _resources.Add(resource); @@ -81,7 +81,7 @@ public ResourceCatalogBuilder AddResources(params Resource[] resources) /// The resource catalog builder. public ResourceCatalogBuilder AddResources(IEnumerable resources) { - _resources ??= new List(); + _resources ??= []; _resources.AddRange(resources); diff --git a/src/extensibility/dotnet-extensibility/Extensibility/DataWriter/DataWriterTypes.cs b/src/extensibility/dotnet-extensibility/Extensibility/DataWriter/DataWriterTypes.cs index 8afc214c..c1dffc37 100644 --- a/src/extensibility/dotnet-extensibility/Extensibility/DataWriter/DataWriterTypes.cs +++ b/src/extensibility/dotnet-extensibility/Extensibility/DataWriter/DataWriterTypes.cs @@ -26,17 +26,10 @@ public record WriteRequest( /// /// An attribute to provide additional information about the data writer. /// +/// The data writer description including the data writer format label and UI options. [AttributeUsage(AttributeTargets.Class)] -public class DataWriterDescriptionAttribute : Attribute +public class DataWriterDescriptionAttribute(string description) : Attribute { - /// - /// 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; } + internal IReadOnlyDictionary? Description { get; } + = JsonSerializer.Deserialize?>(description); } diff --git a/src/extensibility/dotnet-extensibility/Extensibility/ExtensionDescriptionAttribute.cs b/src/extensibility/dotnet-extensibility/Extensibility/ExtensionDescriptionAttribute.cs index b37bf615..3687aca8 100644 --- a/src/extensibility/dotnet-extensibility/Extensibility/ExtensionDescriptionAttribute.cs +++ b/src/extensibility/dotnet-extensibility/Extensibility/ExtensionDescriptionAttribute.cs @@ -3,34 +3,25 @@ /// /// An attribute to identify the extension. /// +/// The extension description. +/// An optional project website URL. +/// An optional source repository URL. [AttributeUsage(validOn: AttributeTargets.Class, AllowMultiple = false)] -public class ExtensionDescriptionAttribute : Attribute +public class ExtensionDescriptionAttribute(string description, string projectUrl, string repositoryUrl) : Attribute { - /// - /// 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; - } /// /// Gets the extension description. /// - public string Description { get; } + public string Description { get; } = description; /// /// Gets the project website URL. /// - public string ProjectUrl { get; } + public string ProjectUrl { get; } = projectUrl; /// /// Gets the source repository URL. /// - public string RepositoryUrl { get; } + public string RepositoryUrl { get; } = repositoryUrl; } \ No newline at end of file diff --git a/tests/Nexus.Tests/DataSource/DataSourceControllerTests.cs b/tests/Nexus.Tests/DataSource/DataSourceControllerTests.cs index d05e5347..e1a61d55 100644 --- a/tests/Nexus.Tests/DataSource/DataSourceControllerTests.cs +++ b/tests/Nexus.Tests/DataSource/DataSourceControllerTests.cs @@ -12,14 +12,10 @@ namespace DataSource; -public class DataSourceControllerTests : IClassFixture +public class DataSourceControllerTests(DataSourceControllerFixture fixture) + : IClassFixture { - private readonly DataSourceControllerFixture _fixture; - - public DataSourceControllerTests(DataSourceControllerFixture fixture) - { - _fixture = fixture; - } + private readonly DataSourceControllerFixture _fixture = fixture; [Fact] internal async Task CanGetAvailability() @@ -132,13 +128,13 @@ public async Task CanRead() // combine var catalogItemRequestPipeWriters = new CatalogItemRequestPipeWriter[] { - new CatalogItemRequestPipeWriter(catalogItemRequest1, dataWriter1), - new CatalogItemRequestPipeWriter(catalogItemRequest2, dataWriter2) + new(catalogItemRequest1, dataWriter1), + new(catalogItemRequest2, dataWriter2) }; var readingGroups = new DataReadingGroup[] { - new DataReadingGroup(controller, catalogItemRequestPipeWriters) + new(controller, catalogItemRequestPipeWriters) }; var result1 = new double[86401]; @@ -458,8 +454,8 @@ public async Task CanReadCached() /* 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) + new(begin, new DateTime(2020, 01, 02, 0, 0, 0, DateTimeKind.Utc)), + new(new DateTime(2020, 01, 03, 0, 0, 0, DateTimeKind.Utc), end) }; var cacheService = new Mock(); diff --git a/tests/Nexus.Tests/DataSource/SampleDataSourceTests.cs b/tests/Nexus.Tests/DataSource/SampleDataSourceTests.cs index 02cd9589..f9fd885a 100644 --- a/tests/Nexus.Tests/DataSource/SampleDataSourceTests.cs +++ b/tests/Nexus.Tests/DataSource/SampleDataSourceTests.cs @@ -29,7 +29,7 @@ public async Task ProvidesCatalog() // 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 actualGroups = actual.Resources!.SelectMany(resource => resource.Properties?.GetStringArray("groups") ?? []); var actualDataTypes = actual.Resources!.SelectMany(resource => resource.Representations!.Select(representation => representation.DataType)).ToList(); var expectedIds = new List() { "T1", "V1", "unix_time1", "unix_time2" }; @@ -110,7 +110,7 @@ public async Task CanReadFullDay() await dataSource.ReadAsync( begin, end, - new[] { request }, + [request], default!, new Progress(), CancellationToken.None); diff --git a/tests/Nexus.Tests/DataWriter/CsvDataWriterTests.cs b/tests/Nexus.Tests/DataWriter/CsvDataWriterTests.cs index 4cb0eb2a..f6a4e489 100644 --- a/tests/Nexus.Tests/DataWriter/CsvDataWriterTests.cs +++ b/tests/Nexus.Tests/DataWriter/CsvDataWriterTests.cs @@ -10,14 +10,10 @@ namespace DataWriter; -public class CsvDataWriterTests : IClassFixture +public class CsvDataWriterTests(DataWriterFixture fixture) + : IClassFixture { - private readonly DataWriterFixture _fixture; - - public CsvDataWriterTests(DataWriterFixture fixture) - { - _fixture = fixture; - } + private readonly DataWriterFixture _fixture = fixture; [Theory] [InlineData("index")] diff --git a/tests/Nexus.Tests/DataWriter/DataWriterControllerTests.cs b/tests/Nexus.Tests/DataWriter/DataWriterControllerTests.cs index c08ef818..026db497 100644 --- a/tests/Nexus.Tests/DataWriter/DataWriterControllerTests.cs +++ b/tests/Nexus.Tests/DataWriter/DataWriterControllerTests.cs @@ -8,14 +8,10 @@ namespace DataWriter; -public class DataWriterControllerTests : IClassFixture +public class DataWriterControllerTests(DataWriterFixture fixture) + : IClassFixture { - private readonly DataWriterFixture _fixture; - - public DataWriterControllerTests(DataWriterFixture fixture) - { - _fixture = fixture; - } + private readonly DataWriterFixture _fixture = fixture; [Fact] public async Task CanWrite() diff --git a/tests/Nexus.Tests/DataWriter/DataWriterFixture.cs b/tests/Nexus.Tests/DataWriter/DataWriterFixture.cs index 12decb9d..e6b9abcc 100644 --- a/tests/Nexus.Tests/DataWriter/DataWriterFixture.cs +++ b/tests/Nexus.Tests/DataWriter/DataWriterFixture.cs @@ -4,15 +4,15 @@ namespace DataWriter; public class DataWriterFixture : IDisposable { - readonly List _targetFolders = new(); + readonly List _targetFolders = []; public DataWriterFixture() { // 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(dataType: NexusDataType.FLOAT32, samplePeriod: TimeSpan.FromSeconds(1)), + new(dataType: NexusDataType.FLOAT64, samplePeriod: TimeSpan.FromSeconds(10)), }; var resourceBuilder1 = new ResourceBuilder(id: "resource1") @@ -37,7 +37,7 @@ public DataWriterFixture() .WithProperty("my-custom-parameter3", "my-custom-value3") .AddResource(resourceBuilder2.Build()); - Catalogs = new[] { catalogBuilder1.Build(), catalogBuilder2.Build() }; + Catalogs = [catalogBuilder1.Build(), catalogBuilder2.Build()]; } public ResourceCatalog[] Catalogs { get; } diff --git a/tests/Nexus.Tests/Other/CatalogContainersExtensionsTests.cs b/tests/Nexus.Tests/Other/CatalogContainersExtensionsTests.cs index c230b240..c27cc1f0 100644 --- a/tests/Nexus.Tests/Other/CatalogContainersExtensionsTests.cs +++ b/tests/Nexus.Tests/Other/CatalogContainersExtensionsTests.cs @@ -25,29 +25,29 @@ public async Task CanTryFindCatalogContainer() { "/" => new CatalogContainer[] { - new CatalogContainer(new CatalogRegistration("/A", string.Empty), default!, default!, default!, default!, catalogManager, default!, default!), + new(new CatalogRegistration("/A", string.Empty), default!, default!, default!, default!, catalogManager, default!, default!), }, - "/A" => new CatalogContainer[] - { + "/A" => + [ 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[] - { + ], + "/A/B" => + [ 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[] - { + ], + "/A/D" => + [ 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[] - { + ], + "/A/F" => + [ new CatalogContainer(new CatalogRegistration("/A/F/H", string.Empty), default!, default!, default!, default!, catalogManager, default!, default!) - }, + ], _ => throw new Exception($"Unsupported combination {container.Id}.") }); }); @@ -123,7 +123,7 @@ public async Task CanTryFind() { "/" => new CatalogContainer[] { - new CatalogContainer(new CatalogRegistration("/A/B/C", string.Empty), default!, default!, default!, default!, default!, default!, dataControllerService), + new(new CatalogRegistration("/A/B/C", string.Empty), default!, default!, default!, default!, default!, default!, dataControllerService), }, _ => throw new Exception("Unsupported combination.") }); diff --git a/tests/Nexus.Tests/Other/LoggingTests.cs b/tests/Nexus.Tests/Other/LoggingTests.cs index 21db804c..a8c4bac8 100644 --- a/tests/Nexus.Tests/Other/LoggingTests.cs +++ b/tests/Nexus.Tests/Other/LoggingTests.cs @@ -46,7 +46,7 @@ public void CanSerilog() Environment.SetEnvironmentVariable("NEXUS_SERILOG__ENRICH__1", "WithMachineName"); // 2. Build the configuration - var configuration = NexusOptionsBase.BuildConfiguration(Array.Empty()); + var configuration = NexusOptionsBase.BuildConfiguration([]); var serilogger = new LoggerConfiguration() .ReadFrom.Configuration(configuration) diff --git a/tests/Nexus.Tests/Other/OptionsTests.cs b/tests/Nexus.Tests/Other/OptionsTests.cs index d5094696..ae051772 100644 --- a/tests/Nexus.Tests/Other/OptionsTests.cs +++ b/tests/Nexus.Tests/Other/OptionsTests.cs @@ -16,7 +16,7 @@ public class OptionsTests public void CanBindOptions(string section, Type optionsType) { var configuration = NexusOptionsBase - .BuildConfiguration(Array.Empty()); + .BuildConfiguration([]); var options = (NexusOptionsBase)configuration .GetSection(section) @@ -29,7 +29,7 @@ public void CanBindOptions(string section, Type optionsType) public void CanReadAppsettingsJson() { var configuration = NexusOptionsBase - .BuildConfiguration(Array.Empty()); + .BuildConfiguration([]); var options = configuration .GetSection(DataOptions.Section) @@ -46,7 +46,7 @@ public void CanOverrideAppsettingsJson_With_Json() Environment.SetEnvironmentVariable("NEXUS_PATHS__SETTINGS", "myappsettings.json"); var configuration = NexusOptionsBase - .BuildConfiguration(Array.Empty()); + .BuildConfiguration([]); var options = configuration .GetSection(DataOptions.Section) @@ -66,7 +66,7 @@ public void CanOverrideIni_With_EnvironmentVariable() Environment.SetEnvironmentVariable("NEXUS_PATHS__SETTINGS", "myappsettings.ini"); var configuration1 = NexusOptionsBase - .BuildConfiguration(Array.Empty()); + .BuildConfiguration([]); var options1 = configuration1 .GetSection(DataOptions.Section) @@ -75,7 +75,7 @@ public void CanOverrideIni_With_EnvironmentVariable() Environment.SetEnvironmentVariable("NEXUS_DATA__AGGREGATIONNANTHRESHOLD", "0.90"); var configuration2 = NexusOptionsBase - .BuildConfiguration(Array.Empty()); + .BuildConfiguration([]); var options2 = configuration2 .GetSection(DataOptions.Section) @@ -105,7 +105,7 @@ public void CanOverrideEnvironmentVariable_With_CommandLineParameter(string arg) Environment.SetEnvironmentVariable("NEXUS_DATA__AGGREGATIONNANTHRESHOLD", "0.90"); var configuration = NexusOptionsBase - .BuildConfiguration(new string[] { arg }); + .BuildConfiguration([arg]); var options = configuration .GetSection(DataOptions.Section) diff --git a/tests/Nexus.Tests/Other/PackageControllerTests.cs b/tests/Nexus.Tests/Other/PackageControllerTests.cs index 473324bf..4df84647 100644 --- a/tests/Nexus.Tests/Other/PackageControllerTests.cs +++ b/tests/Nexus.Tests/Other/PackageControllerTests.cs @@ -195,10 +195,8 @@ private async Task Load_Run_and_Unload_Async( var dataSourceType = assembly .ExportedTypes - .First(type => typeof(IDataSource).IsAssignableFrom(type)); - - if (dataSourceType is null) - throw new Exception("data source type is null"); + .First(type => typeof(IDataSource).IsAssignableFrom(type)) + ?? throw new Exception("data source type is null"); // run diff --git a/tests/Nexus.Tests/Services/CacheServiceTests.cs b/tests/Nexus.Tests/Services/CacheServiceTests.cs index 548ab419..eb0cd801 100644 --- a/tests/Nexus.Tests/Services/CacheServiceTests.cs +++ b/tests/Nexus.Tests/Services/CacheServiceTests.cs @@ -130,8 +130,8 @@ public async Task CanUpdateCache() var expected = new DateTime[] { - new DateTime(2020, 01, 01, 0, 0, 0, DateTimeKind.Utc), - new DateTime(2020, 01, 02, 0, 0, 0, DateTimeKind.Utc) + new(2020, 01, 01, 0, 0, 0, DateTimeKind.Utc), + new(2020, 01, 02, 0, 0, 0, DateTimeKind.Utc) }; var databaseService = Mock.Of(); @@ -167,7 +167,7 @@ public async Task CanUpdateCache() var uncachedIntervals = new List { - new Interval(new DateTime(2020, 01, 01, 15, 0, 0, DateTimeKind.Utc), new DateTime(2020, 01, 02, 03, 0, 0, DateTimeKind.Utc)) + new(new DateTime(2020, 01, 01, 15, 0, 0, DateTimeKind.Utc), new DateTime(2020, 01, 02, 03, 0, 0, DateTimeKind.Utc)) }; var begin = new DateTime(2020, 01, 01, 0, 0, 0, DateTimeKind.Utc); diff --git a/tests/Nexus.Tests/Services/CatalogManagerTests.cs b/tests/Nexus.Tests/Services/CatalogManagerTests.cs index c474bf3f..f548d716 100644 --- a/tests/Nexus.Tests/Services/CatalogManagerTests.cs +++ b/tests/Nexus.Tests/Services/CatalogManagerTests.cs @@ -44,10 +44,10 @@ public async Task CanCreateCatalogHierarchy() return (type, path) switch { - ("A", "/") => Task.FromResult(new CatalogRegistration[] { new CatalogRegistration("/A", string.Empty), new CatalogRegistration("/B/A", string.Empty) }), - ("A", "/A/") => Task.FromResult(new CatalogRegistration[] { new CatalogRegistration("/A/B", string.Empty), new CatalogRegistration("/A/B/C", string.Empty), new CatalogRegistration("/A/C/A", string.Empty) }), - ("B", "/") => Task.FromResult(new CatalogRegistration[] { new CatalogRegistration("/A", string.Empty), new CatalogRegistration("/B/B", string.Empty), new CatalogRegistration("/B/B2", string.Empty) }), - ("C", "/") => Task.FromResult(new CatalogRegistration[] { new CatalogRegistration("/C/A", string.Empty) }), + ("A", "/") => Task.FromResult(new CatalogRegistration[] { new("/A", string.Empty), new("/B/A", string.Empty) }), + ("A", "/A/") => Task.FromResult(new CatalogRegistration[] { new("/A/B", string.Empty), new("/A/B/C", string.Empty), new("/A/C/A", string.Empty) }), + ("B", "/") => Task.FromResult(new CatalogRegistration[] { new("/A", string.Empty), new("/B/B", string.Empty), new("/B/B2", string.Empty) }), + ("C", "/") => Task.FromResult(new CatalogRegistration[] { new("/C/A", string.Empty) }), ("Nexus.Sources." + nameof(Sample), "/") => Task.FromResult(Array.Empty()), _ => throw new Exception("Unsupported combination.") }; @@ -119,8 +119,8 @@ public async Task CanCreateCatalogHierarchy() var userAClaims = new List { - new NexusClaim(Guid.NewGuid(), Claims.Name, usernameA), - new NexusClaim(Guid.NewGuid(), Claims.Role, NexusRoles.ADMINISTRATOR) + new(Guid.NewGuid(), Claims.Name, usernameA), + new(Guid.NewGuid(), Claims.Role, NexusRoles.ADMINISTRATOR) }; var userA = new NexusUser( @@ -135,7 +135,7 @@ public async Task CanCreateCatalogHierarchy() var userBClaims = new List { - new NexusClaim(Guid.NewGuid(), Claims.Name, usernameB), + new(Guid.NewGuid(), Claims.Name, usernameB), }; var userB = new NexusUser( diff --git a/tests/Nexus.Tests/Services/DataServiceTests.cs b/tests/Nexus.Tests/Services/DataServiceTests.cs index 4462b94b..a21dda98 100644 --- a/tests/Nexus.Tests/Services/DataServiceTests.cs +++ b/tests/Nexus.Tests/Services/DataServiceTests.cs @@ -125,7 +125,7 @@ public async Task CanExportAsync() End: end, FilePeriod: TimeSpan.FromSeconds(10), Type: "A", - ResourcePaths: new[] { catalogItem1.ToPath(), catalogItem2.ToPath() }, + ResourcePaths: [catalogItem1.ToPath(), catalogItem2.ToPath()], Configuration: default); // data service diff --git a/tests/Nexus.Tests/Services/MemoryTrackerTests.cs b/tests/Nexus.Tests/Services/MemoryTrackerTests.cs index 157a90ad..aba4e810 100644 --- a/tests/Nexus.Tests/Services/MemoryTrackerTests.cs +++ b/tests/Nexus.Tests/Services/MemoryTrackerTests.cs @@ -32,8 +32,8 @@ public async Task CanHandleMultipleRequests() var firstWaitingTask = memoryTracker.RegisterAllocationAsync(minimumByteCount: 70, maximumByteCount: 70, CancellationToken.None); var secondWaitingTask = memoryTracker.RegisterAllocationAsync(minimumByteCount: 80, maximumByteCount: 80, CancellationToken.None); - Assert.True(firstWaitingTask.Status != TaskStatus.RanToCompletion); - Assert.True(secondWaitingTask.Status != TaskStatus.RanToCompletion); + Assert.NotEqual(TaskStatus.RanToCompletion, firstWaitingTask.Status); + Assert.NotEqual(TaskStatus.RanToCompletion, secondWaitingTask.Status); // dispose first registration weAreWaiting.Set(); diff --git a/tests/Nexus.Tests/Services/TokenServiceTests.cs b/tests/Nexus.Tests/Services/TokenServiceTests.cs index 651deaf0..e95f7aa8 100644 --- a/tests/Nexus.Tests/Services/TokenServiceTests.cs +++ b/tests/Nexus.Tests/Services/TokenServiceTests.cs @@ -16,7 +16,7 @@ public async Task CanCreateToken() { // Arrange var filePath = Path.GetTempFileName(); - var tokenService = GetTokenService(filePath, new()); + var tokenService = GetTokenService(filePath, []); var description = "The description."; var expires = new DateTime(2020, 01, 01); var claim1Type = "claim1"; @@ -31,8 +31,8 @@ await tokenService.CreateAsync( expires, new List { - new TokenClaim(claim1Type, claim1Value), - new TokenClaim(claim2Type, claim2Value), + new(claim1Type, claim1Value), + new(claim2Type, claim2Value), } ); @@ -40,24 +40,20 @@ await tokenService.CreateAsync( var jsonString = File.ReadAllText(filePath); var actualTokenMap = JsonSerializer.Deserialize>(jsonString)!; - Assert.Collection( - actualTokenMap, - entry1 => + var entry1 = Assert.Single(actualTokenMap); + Assert.Equal(description, entry1.Value.Description); + Assert.Equal(expires, entry1.Value.Expires); + + Assert.Collection(entry1.Value.Claims, + entry1_1 => + { + Assert.Equal(claim1Type, entry1_1.Type); + Assert.Equal(claim1Value, entry1_1.Value); + }, + entry1_2 => { - Assert.Equal(description, entry1.Value.Description); - Assert.Equal(expires, entry1.Value.Expires); - - Assert.Collection(entry1.Value.Claims, - entry1_1 => - { - Assert.Equal(claim1Type, entry1_1.Type); - Assert.Equal(claim1Value, entry1_1.Value); - }, - entry1_2 => - { - Assert.Equal(claim2Type, entry1_2.Type); - Assert.Equal(claim2Value, entry1_2.Value); - }); + Assert.Equal(claim2Type, entry1_2.Type); + Assert.Equal(claim2Value, entry1_2.Value); }); } @@ -202,7 +198,7 @@ public async Task CanGetAllTokens() Assert.Equal(expected, actual); } - private ITokenService GetTokenService(string filePath, Dictionary tokenMap) + private static ITokenService GetTokenService(string filePath, Dictionary tokenMap) { var databaseService = Mock.Of(); diff --git a/tests/clients/dotnet-client-tests/ClientTests.cs b/tests/clients/dotnet-client-tests/ClientTests.cs index 914a8565..5fdcd9fe 100644 --- a/tests/clients/dotnet-client-tests/ClientTests.cs +++ b/tests/clients/dotnet-client-tests/ClientTests.cs @@ -75,7 +75,8 @@ public async Task CanAddConfigurationAsync() headers => { Assert.NotNull(headers); - Assert.Collection(headers, header => Assert.Equal(encodedJson, header)); + var header = Assert.Single(headers); + Assert.Equal(encodedJson, header); }, Assert.Null); } diff --git a/tests/extensibility/dotnet-extensibility-tests/DataModelTests.cs b/tests/extensibility/dotnet-extensibility-tests/DataModelTests.cs index 03607a61..763ec25f 100644 --- a/tests/extensibility/dotnet-extensibility-tests/DataModelTests.cs +++ b/tests/extensibility/dotnet-extensibility-tests/DataModelTests.cs @@ -4,14 +4,10 @@ namespace Nexus.Extensibility.Tests; -public class DataModelTests : IClassFixture +public class DataModelTests(DataModelFixture fixture) + : IClassFixture { - private readonly DataModelFixture _fixture; - - public DataModelTests(DataModelFixture fixture) - { - _fixture = fixture; - } + private readonly DataModelFixture _fixture = fixture; [Theory] @@ -200,9 +196,9 @@ static void action() id: "/C", resources: new List() { - new Resource(id: "R1"), - new Resource(id: "R2"), - new Resource(id: "R2") + new(id: "R1"), + new(id: "R2"), + new(id: "R2") }); } @@ -218,16 +214,16 @@ public void ResourceMergeThrowsForNonEqualRepresentations() id: "myresource", representations: new List() { - new Representation(dataType: NexusDataType.FLOAT32, samplePeriod: TimeSpan.FromSeconds(1)) + new(dataType: NexusDataType.FLOAT32, samplePeriod: TimeSpan.FromSeconds(1)) }); 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)) + new(dataType: NexusDataType.FLOAT64, samplePeriod: TimeSpan.FromSeconds(1)), + new(dataType: NexusDataType.FLOAT32, samplePeriod: TimeSpan.FromSeconds(2)), + new(dataType: NexusDataType.FLOAT32, samplePeriod: TimeSpan.FromSeconds(3)) }); // Act @@ -262,8 +258,8 @@ public void CanFindCatalogItem() var catalog = new ResourceCatalog(id: "/A/B/C", resources: new List() { resource }); var catalogItem = new CatalogItem( - catalog with { Resources = default }, - resource with { Representations = default }, + catalog, + resource, representation, Parameters: default);