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
5 changes: 5 additions & 0 deletions SimpleModule.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@
<Project Path="modules/Marketplace/src/SimpleModule.Marketplace/SimpleModule.Marketplace.csproj" />
<Project Path="modules/Marketplace/tests/SimpleModule.Marketplace.Tests/SimpleModule.Marketplace.Tests.csproj" />
</Folder>
<Folder Name="/modules/Tenants/">
<Project Path="modules/Tenants/src/SimpleModule.Tenants.Contracts/SimpleModule.Tenants.Contracts.csproj" />
<Project Path="modules/Tenants/src/SimpleModule.Tenants/SimpleModule.Tenants.csproj" />
<Project Path="modules/Tenants/tests/SimpleModule.Tenants.Tests/SimpleModule.Tenants.Tests.csproj" />
</Folder>
<Folder Name="/template/">
<Project Path="template/SimpleModule.Host/SimpleModule.Host.csproj" />
</Folder>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ public enum OverrideType
{
User = 0,
Role = 1,
Tenant = 2,
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using SimpleModule.Core.Entities;
using SimpleModule.Core.FeatureFlags;
using SimpleModule.FeatureFlags.Contracts;
using SimpleModule.FeatureFlags.Entities;
Expand All @@ -11,16 +13,21 @@ public sealed partial class FeatureFlagService(
FeatureFlagsDbContext db,
IFeatureFlagRegistry registry,
IMemoryCache cache,
ILogger<FeatureFlagService> logger
ILogger<FeatureFlagService> logger,
IServiceProvider serviceProvider
) : IFeatureFlagContracts, IFeatureFlagService
{
private readonly Lazy<ITenantContext?> _tenantContext = new(
() => serviceProvider.GetService<ITenantContext>()
);
private static readonly TimeSpan CacheDuration = TimeSpan.FromSeconds(30);
private const string AllFlagDataCacheKey = "ff:all-data";

private sealed record FlagData(
bool IsEnabled,
Dictionary<string, bool> UserOverrides,
Dictionary<string, bool> RoleOverrides
Dictionary<string, bool> RoleOverrides,
Dictionary<string, bool> TenantOverrides
);

public async Task<bool> IsEnabledAsync(
Expand Down Expand Up @@ -273,23 +280,28 @@ List<FeatureFlagOverrideEntity> overrides
{
var userOverrides = new Dictionary<string, bool>(StringComparer.Ordinal);
var roleOverrides = new Dictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
var tenantOverrides = new Dictionary<string, bool>(StringComparer.Ordinal);

foreach (var o in overrides)
{
if (o.OverrideType == OverrideType.User)
{
userOverrides[o.OverrideValue] = o.IsEnabled;
}
else
switch (o.OverrideType)
{
roleOverrides[o.OverrideValue] = o.IsEnabled;
case OverrideType.User:
userOverrides[o.OverrideValue] = o.IsEnabled;
break;
case OverrideType.Role:
roleOverrides[o.OverrideValue] = o.IsEnabled;
break;
case OverrideType.Tenant:
tenantOverrides[o.OverrideValue] = o.IsEnabled;
break;
}
}

return new FlagData(isEnabled, userOverrides, roleOverrides);
return new FlagData(isEnabled, userOverrides, roleOverrides, tenantOverrides);
}

private static bool ResolveFlagState(
private bool ResolveFlagState(
FlagData data,
string? userId,
IEnumerable<string>? roles
Expand All @@ -311,6 +323,13 @@ private static bool ResolveFlagState(
}
}

var currentTenantId = _tenantContext.Value?.TenantId;
if (currentTenantId is not null
&& data.TenantOverrides.TryGetValue(currentTenantId, out var tenantEnabled))
{
return tenantEnabled;
}

return data.IsEnabled;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using FluentAssertions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using SimpleModule.Core.FeatureFlags;
Expand Down Expand Up @@ -54,7 +55,8 @@ public FeatureFlagServiceTests()
_db,
_registry,
_cache,
NullLogger<FeatureFlagService>.Instance
NullLogger<FeatureFlagService>.Instance,
new ServiceCollection().BuildServiceProvider()
);
}

Expand All @@ -78,7 +80,8 @@ private FeatureFlagService CreateFreshService()
_db,
_registry,
freshCache,
NullLogger<FeatureFlagService>.Instance
NullLogger<FeatureFlagService>.Instance,
new ServiceCollection().BuildServiceProvider()
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace SimpleModule.Tenants.Contracts;

public class AddTenantHostRequest
{
public string HostName { get; set; } = string.Empty;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace SimpleModule.Tenants.Contracts;

public class CreateTenantRequest
{
public string Name { get; set; } = string.Empty;
public string Slug { get; set; } = string.Empty;
public string? AdminEmail { get; set; }
public string? EditionName { get; set; }
public string? ConnectionString { get; set; }
public DateTimeOffset? ValidUpTo { get; set; }
public List<string>? Hosts { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using SimpleModule.Core.Events;

namespace SimpleModule.Tenants.Contracts.Events;

public sealed record TenantCreatedEvent(TenantId TenantId, string Name, string Slug) : IEvent;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using SimpleModule.Core.Events;

namespace SimpleModule.Tenants.Contracts.Events;

public sealed record TenantHostAddedEvent(TenantId TenantId, string HostName) : IEvent;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using SimpleModule.Core.Events;

namespace SimpleModule.Tenants.Contracts.Events;

public sealed record TenantHostRemovedEvent(TenantId TenantId, string HostName) : IEvent;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using SimpleModule.Core.Events;

namespace SimpleModule.Tenants.Contracts.Events;

public sealed record TenantStatusChangedEvent(
TenantId TenantId,
TenantStatus OldStatus,
TenantStatus NewStatus
) : IEvent;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using SimpleModule.Core.Events;

namespace SimpleModule.Tenants.Contracts.Events;

public sealed record TenantUpdatedEvent(TenantId TenantId, string Name) : IEvent;
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace SimpleModule.Tenants.Contracts;

public interface ITenantContracts
{
Task<IEnumerable<Tenant>> GetAllTenantsAsync();
Task<Tenant?> GetTenantByIdAsync(TenantId id);
Task<Tenant?> GetTenantBySlugAsync(string slug);
Task<Tenant?> GetTenantByHostNameAsync(string hostName);
Task<Tenant> CreateTenantAsync(CreateTenantRequest request);
Task<Tenant> UpdateTenantAsync(TenantId id, UpdateTenantRequest request);
Task DeleteTenantAsync(TenantId id);
Task<Tenant> ChangeStatusAsync(TenantId id, TenantStatus status);
Task<TenantHost> AddHostAsync(TenantId tenantId, AddTenantHostRequest request);
Task RemoveHostAsync(TenantId tenantId, TenantHostId hostId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<OutputType>Library</OutputType>
<DefineConstants>$(DefineConstants);VOGEN_NO_VALIDATION</DefineConstants>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" />
<PackageReference Include="Vogen" />
<ProjectReference Include="..\..\..\..\framework\SimpleModule.Core\SimpleModule.Core.csproj" />
</ItemGroup>
</Project>
19 changes: 19 additions & 0 deletions modules/Tenants/src/SimpleModule.Tenants.Contracts/Tenant.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using SimpleModule.Core;

namespace SimpleModule.Tenants.Contracts;

[Dto]
public class Tenant
{
public TenantId Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Slug { get; set; } = string.Empty;
public TenantStatus Status { get; set; }
public string? AdminEmail { get; set; }
public string? EditionName { get; set; }
public string? ConnectionString { get; set; }
public DateTimeOffset CreatedAt { get; set; }
public DateTimeOffset UpdatedAt { get; set; }
public DateTimeOffset? ValidUpTo { get; set; }
public List<TenantHost> Hosts { get; set; } = [];
}
12 changes: 12 additions & 0 deletions modules/Tenants/src/SimpleModule.Tenants.Contracts/TenantHost.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using SimpleModule.Core;

namespace SimpleModule.Tenants.Contracts;

[Dto]
public class TenantHost
{
public TenantHostId Id { get; set; }
public TenantId TenantId { get; set; }
public string HostName { get; set; } = string.Empty;
public bool IsActive { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
using Vogen;

namespace SimpleModule.Tenants.Contracts;

[ValueObject<int>(conversions: Conversions.SystemTextJson | Conversions.EfCoreValueConverter)]
public readonly partial struct TenantHostId;
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
using Vogen;

namespace SimpleModule.Tenants.Contracts;

[ValueObject<int>(conversions: Conversions.SystemTextJson | Conversions.EfCoreValueConverter)]
public readonly partial struct TenantId;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace SimpleModule.Tenants.Contracts;

public enum TenantStatus
{
Active = 0,
Suspended = 1,
Inactive = 2,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using SimpleModule.Core.Authorization;

namespace SimpleModule.Tenants.Contracts;

public sealed class TenantsPermissions : IModulePermissions
{
public const string View = "Tenants.View";
public const string Create = "Tenants.Create";
public const string Update = "Tenants.Update";
public const string Delete = "Tenants.Delete";
public const string ChangeStatus = "Tenants.ChangeStatus";
public const string ManageHosts = "Tenants.ManageHosts";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace SimpleModule.Tenants.Contracts;

public class UpdateTenantRequest
{
public string Name { get; set; } = string.Empty;
public string? AdminEmail { get; set; }
public string? EditionName { get; set; }
public string? ConnectionString { get; set; }
public DateTimeOffset? ValidUpTo { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using SimpleModule.Core;
using SimpleModule.Core.Authorization;
using SimpleModule.FeatureFlags.Contracts;
using SimpleModule.Tenants.Contracts;

namespace SimpleModule.Tenants.Endpoints.TenantFeatures;

public class DeleteTenantFeatureEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder app) =>
app.MapDelete(
"/{id}/features/{flagName}",
async (TenantId id, string flagName, HttpContext context) =>
{
var featureFlags = context.RequestServices.GetService<IFeatureFlagContracts>();
if (featureFlags is null)
{
return Results.NotFound();
}

var tenantIdStr = id.Value.ToString(System.Globalization.CultureInfo.InvariantCulture);
var overrides = await featureFlags.GetOverridesAsync(flagName);
var tenantOverride = overrides.FirstOrDefault(o =>
o.OverrideType == OverrideType.Tenant
&& string.Equals(o.OverrideValue, tenantIdStr, StringComparison.Ordinal)
);

if (tenantOverride is null)
{
return Results.NotFound();
}

await featureFlags.DeleteOverrideAsync(tenantOverride.Id);
return Results.NoContent();
}
)
.RequirePermission(TenantsPermissions.Update);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using SimpleModule.Core;
using SimpleModule.Core.Authorization;
using SimpleModule.FeatureFlags.Contracts;
using SimpleModule.Tenants.Contracts;

namespace SimpleModule.Tenants.Endpoints.TenantFeatures;

public class GetTenantFeaturesEndpoint : IEndpoint
{
public void Map(IEndpointRouteBuilder app) =>
app.MapGet(
"/{id}/features",
async (TenantId id, HttpContext context) =>
{
var featureFlags = context.RequestServices.GetService<IFeatureFlagContracts>();
if (featureFlags is null)
{
return Results.Ok(new TenantFeaturesResponse([], []));
}

var flags = (await featureFlags.GetAllFlagsAsync()).ToList();
var tenantOverrides = await TenantFeatureHelper.GetOverridesForTenantAsync(
featureFlags, flags, id);

return Results.Ok(new TenantFeaturesResponse(flags, tenantOverrides));
}
)
.RequirePermission(TenantsPermissions.View);
}

public sealed record TenantFeaturesResponse(
IEnumerable<FeatureFlag> Flags,
IEnumerable<FeatureFlagOverride> TenantOverrides
);
Loading
Loading