Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/BuildingBlocks/Jobs/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,8 @@ public static IApplicationBuilder UseHeroJobDashboard(this IApplicationBuilder a
{
new HangfireCustomBasicAuthenticationFilter
{
User = hangfireOptions.UserName!,
Pass = hangfireOptions.Password!
User = hangfireOptions.UserName,
Pass = hangfireOptions.Password
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public HangfireCustomBasicAuthenticationFilter()
public bool Authorize(DashboardContext context)
{
var httpContext = context.GetHttpContext();
var header = httpContext.Request.Headers.Authorization!;
var header = httpContext.Request.Headers.Authorization;

if (MissingAuthorizationHeader(header))
{
Expand All @@ -33,7 +33,7 @@ public bool Authorize(DashboardContext context)
return false;
}

var authValues = AuthenticationHeaderValue.Parse(header!);
var authValues = AuthenticationHeaderValue.Parse(header.ToString());

if (NotBasicAuthentication(authValues))
{
Expand Down
2 changes: 1 addition & 1 deletion src/BuildingBlocks/Quota/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public static IServiceCollection AddHeroQuotas(this IServiceCollection services,
{
services.AddSingleton<IConnectionMultiplexer>(_ =>
{
var config = ConfigurationOptions.Parse(quotaOptions.Redis!);
var config = ConfigurationOptions.Parse(quotaOptions.Redis);
config.AbortOnConnectFail = false;
return ConnectionMultiplexer.Connect(config);
});
Expand Down
2 changes: 1 addition & 1 deletion src/BuildingBlocks/Quota/QuotaPlanResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public long ResolveLimit(AppTenantInfo? tenant, QuotaResource resource)
return NormalizeLimit(tenantLimit);
}

var planName = !string.IsNullOrWhiteSpace(tenant?.Plan) ? tenant!.Plan! : _options.DefaultPlan;
var planName = !string.IsNullOrWhiteSpace(tenant?.Plan) ? tenant.Plan : _options.DefaultPlan;

if (_options.Plans.TryGetValue(planName, out var plan)
&& plan.TryGetValue(resource, out var planLimit))
Expand Down
2 changes: 1 addition & 1 deletion src/BuildingBlocks/Storage/Local/LocalStorageService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ public string BuildPublicUrl(string storageKey)

return Task.FromResult<StoredObjectMetadata?>(new StoredObjectMetadata(
info.Length,
contentType!,
contentType,
new DateTimeOffset(info.LastWriteTimeUtc, TimeSpan.Zero),
ETag: null));
}
Expand Down
3 changes: 3 additions & 0 deletions src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
<!-- Docs -->
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);CS1591;MSG0005;CA1054;CA1056</NoWarn>
<!-- AV0029/AV0030 (Asp.Versioning 10.2+): advisories toward AddApiVersioning().AddOpenApi(). FSH deliberately registers one
AddOpenApi(groupName) document per version (Web/OpenApi/Extensions.cs); also fires inside the OpenApi source-generated file. -->
<NoWarn>$(NoWarn);AV0029;AV0030</NoWarn>
<!-- Suppress warning about missing XML docs -->

<!-- Container tags (if using container builds) -->
Expand Down
154 changes: 77 additions & 77 deletions src/Directory.Packages.props

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion src/Host/FSH.Starter.AppHost/FSH.Starter.AppHost.csproj
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
<Project Sdk="Aspire.AppHost.Sdk/13.4.0">
<Project Sdk="Aspire.AppHost.Sdk/13.5.4">

<PropertyGroup>
<OutputType>Exe</OutputType>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<UserSecretsId>9fe5df9a-b9b2-4202-bdb4-d30b01b71d1a</UserSecretsId>
<IsPackable>false</IsPackable>
<!-- ASPIRE010 (13.5+): we run via `dotnet run` with NuGet-restored DCP/dashboard, not the Aspire CLI bundle. -->
<NoWarn>$(NoWarn);ASPIRE010</NoWarn>
</PropertyGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public static List<Diff> Build(IEnumerable<EntityEntry> entries)
return new Diff(
DbContext: entry.Context.GetType().Name,
Schema: schema,
Table: table!,
Table: table,
EntityName: entityType.ClrType.Name,
Key: key,
Operation: operation,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ private async Task SeedAdminUserAsync(CancellationToken cancellationToken = defa
return;
}

if (await userManager.Users.FirstOrDefaultAsync(u => u.Email == multiTenantContextAccessor.MultiTenantContext.TenantInfo!.AdminEmail, cancellationToken)
if (await userManager.Users.FirstOrDefaultAsync(u => u.Email == multiTenantContextAccessor.MultiTenantContext.TenantInfo.AdminEmail, cancellationToken)
is not FshUser adminUser)
{
string adminUserName = $"{multiTenantContextAccessor.MultiTenantContext.TenantInfo?.Id.Trim()}.{RoleConstants.Admin}".ToUpperInvariant();
Expand All @@ -194,7 +194,7 @@ private async Task SeedAdminUserAsync(CancellationToken cancellationToken = defa
UserName = adminUserName,
EmailConfirmed = true,
PhoneNumberConfirmed = true,
NormalizedEmail = multiTenantContextAccessor.MultiTenantContext.TenantInfo?.AdminEmail!.ToUpperInvariant(),
NormalizedEmail = multiTenantContextAccessor.MultiTenantContext.TenantInfo?.AdminEmail.ToUpperInvariant(),
NormalizedUserName = adminUserName.ToUpperInvariant(),
// No default avatar: the asset was never shipped, and baking an absolute
// {OriginUrl}/… URL at seed time pinned it to the seeder's localhost origin
Expand All @@ -207,7 +207,7 @@ private async Task SeedAdminUserAsync(CancellationToken cancellationToken = defa
{
logger.LogInformation("Seeding Default Admin User for '{TenantId}' Tenant.", multiTenantContextAccessor.MultiTenantContext.TenantInfo?.Id);
}
var initialPassword = ResolveInitialAdminPassword(multiTenantContextAccessor.MultiTenantContext.TenantInfo!.Id!);
var initialPassword = ResolveInitialAdminPassword(multiTenantContextAccessor.MultiTenantContext.TenantInfo!.Id);
var password = new PasswordHasher<FshUser>();
adminUser.PasswordHash = password.HashPassword(adminUser, initialPassword);
// MUST check IdentityResult: a silent failure (password-policy reject, transient DB error)
Expand All @@ -216,7 +216,7 @@ private async Task SeedAdminUserAsync(CancellationToken cancellationToken = defa
if (!createResult.Succeeded)
{
throw new InvalidOperationException(
$"Failed to seed admin user for tenant '{multiTenantContextAccessor.MultiTenantContext.TenantInfo!.Id}': "
$"Failed to seed admin user for tenant '{multiTenantContextAccessor.MultiTenantContext.TenantInfo.Id}': "
+ string.Join("; ", createResult.Errors.Select(e => e.Description)));
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public async ValueTask<RefreshTokenCommandResponse> Handle(

if (validated is null)
{
await _securityAudit.TokenRevokedAsync("unknown", clientId!, "InvalidRefreshToken", cancellationToken);
await _securityAudit.TokenRevokedAsync("unknown", clientId, "InvalidRefreshToken", cancellationToken);
throw new UnauthorizedException("Invalid refresh token.");
}

Expand All @@ -61,7 +61,7 @@ public async ValueTask<RefreshTokenCommandResponse> Handle(
var isSessionValid = await _sessionService.ValidateSessionAsync(refreshTokenHash, cancellationToken);
if (!isSessionValid)
{
await _securityAudit.TokenRevokedAsync(subject, clientId!, "SessionRevoked", cancellationToken);
await _securityAudit.TokenRevokedAsync(subject, clientId, "SessionRevoked", cancellationToken);
throw new UnauthorizedException("Session has been revoked.");
}

Expand All @@ -86,13 +86,13 @@ public async ValueTask<RefreshTokenCommandResponse> Handle(
if (!string.IsNullOrEmpty(accessTokenSubject) &&
!string.Equals(accessTokenSubject, subject, StringComparison.Ordinal))
{
await _securityAudit.TokenRevokedAsync(subject, clientId!, "RefreshTokenSubjectMismatch", cancellationToken);
await _securityAudit.TokenRevokedAsync(subject, clientId, "RefreshTokenSubjectMismatch", cancellationToken);
throw new UnauthorizedException("Access token subject mismatch.");
}
}

// Audit previous token revocation by rotation (no raw tokens)
await _securityAudit.TokenRevokedAsync(subject, clientId!, "RefreshTokenRotated", cancellationToken);
await _securityAudit.TokenRevokedAsync(subject, clientId, "RefreshTokenRotated", cancellationToken);

// Issue new tokens
var newToken = await _tokenService.IssueAsync(subject, claims, null, cancellationToken);
Expand All @@ -113,7 +113,7 @@ await _sessionService.UpdateSessionRefreshTokenAsync(
await _securityAudit.TokenIssuedAsync(
userId: subject,
userName: claims.FirstOrDefault(c => c.Type == ClaimTypes.Name)?.Value ?? string.Empty,
clientId: clientId!,
clientId: clientId,
tokenFingerprint: fingerprint,
expiresUtc: newToken.AccessTokenExpiresAt,
ct: cancellationToken);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ public async ValueTask<TokenResponse> Handle(
// 1) Audit failed login BEFORE throwing
await _securityAudit.LoginFailedAsync(
subjectIdOrName: request.Email,
clientId: clientId!,
clientId: clientId,
reason: "InvalidCredentials",
ip: ip,
ct: cancellationToken);
Expand All @@ -80,7 +80,7 @@ await _securityAudit.LoginFailedAsync(
await _securityAudit.LoginSucceededAsync(
userId: subject,
userName: claims.FirstOrDefault(c => c.Type == ClaimTypes.Name)?.Value ?? request.Email,
clientId: clientId!,
clientId: clientId,
ip: ip,
userAgent: ua,
ct: cancellationToken);
Expand Down Expand Up @@ -115,7 +115,7 @@ await _sessionService.CreateSessionAsync(
await _securityAudit.TokenIssuedAsync(
userId: subject,
userName: claims.FirstOrDefault(c => c.Type == ClaimTypes.Name)?.Value ?? request.Email,
clientId: clientId!,
clientId: clientId,
tokenFingerprint: fingerprint,
expiresUtc: token.AccessTokenExpiresAt,
ct: cancellationToken);
Expand All @@ -132,7 +132,7 @@ await _securityAudit.TokenIssuedAsync(
Source: "Identity",
UserId: subject,
Email: request.Email,
ClientId: clientId!,
ClientId: clientId,
IpAddress: ip,
UserAgent: ua,
TokenFingerprint: fingerprint,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public async ValueTask<CreateTenantCommandResponse> Handle(CreateTenantCommand c
// window. A bad plan key throws NotFound (400) before any tenant is created.
var planKey = string.IsNullOrWhiteSpace(command.PlanKey)
? billingOptions.Value.DefaultPlanKey
: command.PlanKey!;
: command.PlanKey;
var term = await mediator.Send(new GetPlanTermQuery(planKey), cancellationToken).ConfigureAwait(false);

var periodStart = timeProvider.GetUtcNow().UtcDateTime;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public CreateTenantCommandValidator(ITenantService tenantService, IConnectionStr

RuleFor(t => t.Name).Cascade(CascadeMode.Stop)
.NotEmpty()
.MustAsync(async (name, ct) => !await tenantService.ExistsWithNameAsync(name!, ct).ConfigureAwait(false))
.MustAsync(async (name, ct) => !await tenantService.ExistsWithNameAsync(name, ct).ConfigureAwait(false))
.WithMessage((_, name) => $"Tenant {name} already exists.");

RuleFor(t => t.ConnectionString).Cascade(CascadeMode.Stop)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ internal sealed class GetTenantsSpecification : Specification<AppTenantInfo, Ten
new Dictionary<string, Expression<Func<AppTenantInfo, object>>>(
StringComparer.OrdinalIgnoreCase)
{
["id"] = t => t.Id!,
["id"] = t => t.Id,
["name"] = t => t.Name!,
["connectionstring"] = t => t.ConnectionString!,
["adminemail"] = t => t.AdminEmail!,
["connectionstring"] = t => t.ConnectionString,
["adminemail"] = t => t.AdminEmail,
["isactive"] = t => t.IsActive,
["validupto"] = t => t.ValidUpto,
["issuer"] = t => t.Issuer!
Expand All @@ -29,10 +29,10 @@ public GetTenantsSpecification(GetTenantsQuery query)
// Default projection to TenantDto.
Select(t => new TenantDto
{
Id = t.Id!,
Id = t.Id,
Name = t.Name!,
ConnectionString = t.ConnectionString,
AdminEmail = t.AdminEmail!,
AdminEmail = t.AdminEmail,
IsActive = t.IsActive,
ValidUpto = t.ValidUpto,
Issuer = t.Issuer
Expand All @@ -46,7 +46,7 @@ public GetTenantsSpecification(GetTenantsQuery query)
() =>
{
OrderBy(t => t.Name!);
ThenBy(t => t.Id!);
ThenBy(t => t.Id);
},
SortMappings);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ public async ValueTask<RenewTenantCommandResponse> Handle(RenewTenantCommand com
var targetKey = command.PlanKey;
if (string.IsNullOrWhiteSpace(targetKey))
{
targetKey = string.IsNullOrWhiteSpace(status.Plan) ? billingOptions.Value.DefaultPlanKey : status.Plan!;
targetKey = string.IsNullOrWhiteSpace(status.Plan) ? billingOptions.Value.DefaultPlanKey : status.Plan;
}

var term = await mediator.Send(new GetPlanTermQuery(targetKey!), cancellationToken).ConfigureAwait(false);
var term = await mediator.Send(new GetPlanTermQuery(targetKey), cancellationToken).ConfigureAwait(false);

var (periodStart, validUpto, planChanged) = await tenantService
.RenewAsync(command.TenantId, term.Key, term.TermMonths, cancellationToken).ConfigureAwait(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ public void ConfigureServices(IHostApplicationBuilder builder)
if (context.MultiTenantContext.StoreInfo is null) return;
if (context.MultiTenantContext.StoreInfo.StoreType != typeof(DistributedCacheStore<AppTenantInfo>))
{
var sp = ((HttpContext)context.Context!).RequestServices;
var sp = ((HttpContext)context.Context).RequestServices;
var distributedStore = sp
.GetRequiredService<IEnumerable<IMultiTenantStore<AppTenantInfo>>>()
.FirstOrDefault(s => s.GetType() == typeof(DistributedCacheStore<AppTenantInfo>));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ public async Task<TenantProvisioning> StartAsync(string tenantId, CancellationTo
return provisioning;
}

var jobId = _jobService.Enqueue<TenantProvisioningJob>(job => job.RunAsync(tenant.Id, correlationId));
// CancellationToken.None: Hangfire substitutes its own job-cancellation token at execution time; the request token must not flow into the job.
var jobId = _jobService.Enqueue<TenantProvisioningJob>(job => job.RunAsync(tenant.Id, correlationId, CancellationToken.None));
provisioning.SetJobId(jobId);
await _dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,12 +184,12 @@ public async Task<TenantStatusDto> GetStatusAsync(string id, CancellationToken c

return new TenantStatusDto
{
Id = tenant.Id!,
Id = tenant.Id,
Name = tenant.Name!,
IsActive = tenant.IsActive,
ValidUpto = tenant.ValidUpto,
HasConnectionString = !string.IsNullOrWhiteSpace(tenant.ConnectionString),
AdminEmail = tenant.AdminEmail!,
AdminEmail = tenant.AdminEmail,
Issuer = tenant.Issuer,
Plan = tenant.Plan,
ExpiryState = expiryState,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,10 @@ public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context
bool hasPending = pendingMigrations.Length > 0;
if (hasPending)
{
tenantsWithPending.Add(tenant.Id!);
tenantsWithPending.Add(tenant.Id);
}

details[tenant.Id!] = new
details[tenant.Id] = new
{
tenant.Name,
tenant.IsActive,
Expand All @@ -70,8 +70,8 @@ public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context
// detail entries so the readiness payload tells the operator which tenant is broken.
catch (Exception ex)
{
tenantsWithError.Add(tenant.Id!);
details[tenant.Id!] = new
tenantsWithError.Add(tenant.Id);
details[tenant.Id] = new
{
tenant.Name,
tenant.IsActive,
Expand Down
2 changes: 1 addition & 1 deletion src/Tests/Architecture.Tests/NamespaceConventionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public void BuildingBlocks_Core_Domain_Namespaces_Should_Match_Folder()

namespaceLine.ShouldNotBeNull($"File '{file}' must declare a namespace matching the folder structure.");

string declaredNamespace = namespaceLine!["namespace ".Length..].Trim().TrimEnd(';');
string declaredNamespace = namespaceLine["namespace ".Length..].Trim().TrimEnd(';');

declaredNamespace
.Contains(".Core.", StringComparison.Ordinal)
Expand Down
2 changes: 1 addition & 1 deletion src/Tests/Billing.Tests/Domain/InvoiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ public void Issue_Should_Default_Due_Date_To_14_Days_After_Issue()
inv.Status.ShouldBe(InvoiceStatus.Issued);
inv.IssuedAtUtc.ShouldNotBeNull();
inv.DueAtUtc.ShouldNotBeNull();
(inv.DueAtUtc!.Value - inv.IssuedAtUtc!.Value).Days.ShouldBe(14);
(inv.DueAtUtc.Value - inv.IssuedAtUtc.Value).Days.ShouldBe(14);
}

[Fact]
Expand Down
2 changes: 1 addition & 1 deletion src/Tests/Billing.Tests/Domain/SubscriptionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public void Cancel_Should_Set_Status_Cancelled_And_Normalize_EndUtc_To_Utc()

sub.Status.ShouldBe(SubscriptionStatus.Cancelled);
sub.EndUtc.ShouldNotBeNull();
sub.EndUtc!.Value.Kind.ShouldBe(DateTimeKind.Utc);
sub.EndUtc.Value.Kind.ShouldBe(DateTimeKind.Utc);
sub.UpdatedAtUtc.ShouldNotBeNull();
}

Expand Down
2 changes: 1 addition & 1 deletion src/Tests/Caching.Tests/CachedTypeContractTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ public void CachedType_Should_HaveImmutableObjectAttribute(Type type)

attr.ShouldNotBeNull(
$"{type.FullName} is stored in HybridCache and must have [ImmutableObject(true)] so the runtime can return the same reference across L1 hits instead of re-deserializing.");
attr!.Immutable.ShouldBeTrue(
attr.Immutable.ShouldBeTrue(
$"{type.FullName} has [ImmutableObject(false)] — change to [ImmutableObject(true)] or stop caching it.");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public void Maps_OutboxMessages_To_Framework_Schema()
var entity = context.Model.FindEntityType(typeof(OutboxMessage));

entity.ShouldNotBeNull();
entity!.GetSchema().ShouldBe(EventingConstants.SchemaName);
entity.GetSchema().ShouldBe(EventingConstants.SchemaName);
entity.GetTableName().ShouldBe("OutboxMessages");
}

Expand All @@ -57,7 +57,7 @@ public void Outbox_Has_Claim_Index_For_Pending_Scan()
var entity = context.Model.FindEntityType(typeof(OutboxMessage));

entity.ShouldNotBeNull();
entity!.GetIndexes()
entity.GetIndexes()
.Any(i => i.GetDatabaseName() == "IX_OutboxMessages_Pending")
.ShouldBeTrue("the claim scan filters and orders on these columns under a row lock");
}
Expand All @@ -69,7 +69,7 @@ public void Maps_InboxMessages_To_Framework_Schema()
var entity = context.Model.FindEntityType(typeof(InboxMessage));

entity.ShouldNotBeNull();
entity!.GetSchema().ShouldBe(EventingConstants.SchemaName);
entity.GetSchema().ShouldBe(EventingConstants.SchemaName);
entity.GetTableName().ShouldBe("InboxMessages");
}

Expand Down
Loading
Loading