diff --git a/src/GovUK.Dfe.FlexForms.Api.Client/Generated/Client.g.cs b/src/GovUK.Dfe.FlexForms.Api.Client/Generated/Client.g.cs
index 5d9a8a78..34940fb8 100644
--- a/src/GovUK.Dfe.FlexForms.Api.Client/Generated/Client.g.cs
+++ b/src/GovUK.Dfe.FlexForms.Api.Client/Generated/Client.g.cs
@@ -10788,7 +10788,7 @@ public string BaseUrl
///
/// Tenant users.
/// A server side error occurred.
- public virtual async System.Threading.Tasks.Task GetTenantUsersAsync(int? pageNumber = null, int? pageSize = null, System.Guid? userId = null, string email = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken))
+ public virtual async System.Threading.Tasks.Task GetTenantUsersAsync(int? pageNumber = null, int? pageSize = null, System.Guid? userId = null, string email = null, string searchTerm = null, string role = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken))
{
var client_ = _httpClient;
var disposeClient_ = false;
@@ -10820,6 +10820,14 @@ public string BaseUrl
{
urlBuilder_.Append(System.Uri.EscapeDataString("email")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(email, System.Globalization.CultureInfo.InvariantCulture))).Append('&');
}
+ if (searchTerm != null)
+ {
+ urlBuilder_.Append(System.Uri.EscapeDataString("searchTerm")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(searchTerm, System.Globalization.CultureInfo.InvariantCulture))).Append('&');
+ }
+ if (role != null)
+ {
+ urlBuilder_.Append(System.Uri.EscapeDataString("role")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(role, System.Globalization.CultureInfo.InvariantCulture))).Append('&');
+ }
urlBuilder_.Length--;
PrepareRequest(client_, request_, urlBuilder_);
diff --git a/src/GovUK.Dfe.FlexForms.Api.Client/Generated/Contracts.g.cs b/src/GovUK.Dfe.FlexForms.Api.Client/Generated/Contracts.g.cs
index d1ed4eeb..3c6b9ebf 100644
--- a/src/GovUK.Dfe.FlexForms.Api.Client/Generated/Contracts.g.cs
+++ b/src/GovUK.Dfe.FlexForms.Api.Client/Generated/Contracts.g.cs
@@ -721,7 +721,7 @@ public partial interface IUsersClient
///
/// Tenant users.
/// A server side error occurred.
- System.Threading.Tasks.Task GetTenantUsersAsync(int? pageNumber = null, int? pageSize = null, System.Guid? userId = null, string email = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));
+ System.Threading.Tasks.Task GetTenantUsersAsync(int? pageNumber = null, int? pageSize = null, System.Guid? userId = null, string email = null, string searchTerm = null, string role = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));
/// A cancellation token that can be used by other objects or threads to receive notice of cancellation.
///
diff --git a/src/GovUK.Dfe.FlexForms.Api.Client/Generated/swagger.json b/src/GovUK.Dfe.FlexForms.Api.Client/Generated/swagger.json
index e952094e..82280b8f 100644
--- a/src/GovUK.Dfe.FlexForms.Api.Client/Generated/swagger.json
+++ b/src/GovUK.Dfe.FlexForms.Api.Client/Generated/swagger.json
@@ -5462,6 +5462,24 @@
"nullable": true
},
"x-position": 4
+ },
+ {
+ "name": "searchTerm",
+ "in": "query",
+ "schema": {
+ "type": "string",
+ "nullable": true
+ },
+ "x-position": 5
+ },
+ {
+ "name": "role",
+ "in": "query",
+ "schema": {
+ "type": "string",
+ "nullable": true
+ },
+ "x-position": 6
}
],
"responses": {
diff --git a/src/GovUK.Dfe.FlexForms.Api/Controllers/UsersController.cs b/src/GovUK.Dfe.FlexForms.Api/Controllers/UsersController.cs
index 5ee4168a..3bfffb9d 100644
--- a/src/GovUK.Dfe.FlexForms.Api/Controllers/UsersController.cs
+++ b/src/GovUK.Dfe.FlexForms.Api/Controllers/UsersController.cs
@@ -137,6 +137,8 @@ public async Task>> GetTenantUsersAsync(
[FromQuery] int? pageSize,
[FromQuery] Guid? userId,
[FromQuery] string? email,
+ [FromQuery] string? searchTerm,
+ [FromQuery] string? role,
CancellationToken cancellationToken)
{
var result = await sender.Send(
@@ -144,7 +146,9 @@ public async Task>> GetTenantUsersAsync(
pageNumber ?? 1,
pageSize ?? GetTenantUsersQuery.DefaultPageSize,
userId,
- email),
+ email,
+ searchTerm,
+ role),
cancellationToken);
if (!result.IsSuccess)
diff --git a/src/GovUK.Dfe.FlexForms.Application/TenantMemberships/QueryObjects/GetActiveTenantMembershipsForDirectoryQueryObject.cs b/src/GovUK.Dfe.FlexForms.Application/TenantMemberships/QueryObjects/GetActiveTenantMembershipsForDirectoryQueryObject.cs
index 2b0de10c..9fe4854e 100644
--- a/src/GovUK.Dfe.FlexForms.Application/TenantMemberships/QueryObjects/GetActiveTenantMembershipsForDirectoryQueryObject.cs
+++ b/src/GovUK.Dfe.FlexForms.Application/TenantMemberships/QueryObjects/GetActiveTenantMembershipsForDirectoryQueryObject.cs
@@ -12,7 +12,9 @@ namespace GovUK.Dfe.FlexForms.Application.TenantMemberships.QueryObjects;
public sealed class GetActiveTenantMembershipsForDirectoryQueryObject(
Guid tenantId,
UserId? userId = null,
- string? email = null)
+ string? email = null,
+ string? searchTerm = null,
+ string? role = null)
: IQueryObject
{
public IQueryable Apply(IQueryable query)
@@ -32,6 +34,12 @@ public IQueryable Apply(IQueryable query)
query = query.Where(m => m.User != null && m.User.Email.ToLower() == normalized);
}
+ if (!string.IsNullOrWhiteSpace(searchTerm))
+ query = new GetTenantMembershipsBySearchTermQueryObject(searchTerm).Apply(query);
+
+ if (!string.IsNullOrWhiteSpace(role))
+ query = new GetTenantMembershipsByRoleNameQueryObject(role).Apply(query);
+
return query
.OrderBy(m => m.User!.Name)
.ThenBy(m => m.User!.Email);
diff --git a/src/GovUK.Dfe.FlexForms.Application/TenantMemberships/QueryObjects/GetTenantMembershipsByRoleNameQueryObject.cs b/src/GovUK.Dfe.FlexForms.Application/TenantMemberships/QueryObjects/GetTenantMembershipsByRoleNameQueryObject.cs
new file mode 100644
index 00000000..d5c4e9a5
--- /dev/null
+++ b/src/GovUK.Dfe.FlexForms.Application/TenantMemberships/QueryObjects/GetTenantMembershipsByRoleNameQueryObject.cs
@@ -0,0 +1,16 @@
+using GovUK.Dfe.FlexForms.Application.Common.QueriesObjects;
+using GovUK.Dfe.FlexForms.Domain.Entities;
+
+namespace GovUK.Dfe.FlexForms.Application.TenantMemberships.QueryObjects;
+
+///
+/// Restricts memberships to a single role, matched on the role name assigned within the tenant.
+///
+public sealed class GetTenantMembershipsByRoleNameQueryObject(string roleName)
+ : IQueryObject
+{
+ private readonly string _roleName = roleName.Trim().ToLowerInvariant();
+
+ public IQueryable Apply(IQueryable query) =>
+ query.Where(m => m.Role != null && m.Role.Name.ToLower() == _roleName);
+}
diff --git a/src/GovUK.Dfe.FlexForms.Application/TenantMemberships/QueryObjects/GetTenantMembershipsBySearchTermQueryObject.cs b/src/GovUK.Dfe.FlexForms.Application/TenantMemberships/QueryObjects/GetTenantMembershipsBySearchTermQueryObject.cs
new file mode 100644
index 00000000..5737f93a
--- /dev/null
+++ b/src/GovUK.Dfe.FlexForms.Application/TenantMemberships/QueryObjects/GetTenantMembershipsBySearchTermQueryObject.cs
@@ -0,0 +1,19 @@
+using GovUK.Dfe.FlexForms.Application.Common.QueriesObjects;
+using GovUK.Dfe.FlexForms.Domain.Entities;
+
+namespace GovUK.Dfe.FlexForms.Application.TenantMemberships.QueryObjects;
+
+///
+/// Free-text filter matching part of the member's name or email address.
+///
+public sealed class GetTenantMembershipsBySearchTermQueryObject(string searchTerm)
+ : IQueryObject
+{
+ private readonly string _searchTerm = searchTerm.Trim().ToLowerInvariant();
+
+ public IQueryable Apply(IQueryable query) =>
+ query.Where(m =>
+ m.User != null
+ && (m.User.Name.ToLower().Contains(_searchTerm)
+ || m.User.Email.ToLower().Contains(_searchTerm)));
+}
diff --git a/src/GovUK.Dfe.FlexForms.Application/Users/Queries/GetTenantUsersQueryHandler.cs b/src/GovUK.Dfe.FlexForms.Application/Users/Queries/GetTenantUsersQueryHandler.cs
index 55e216d4..bf2d4be8 100644
--- a/src/GovUK.Dfe.FlexForms.Application/Users/Queries/GetTenantUsersQueryHandler.cs
+++ b/src/GovUK.Dfe.FlexForms.Application/Users/Queries/GetTenantUsersQueryHandler.cs
@@ -23,12 +23,16 @@ public sealed record GetTenantUsersQuery(
int PageNumber = 1,
int PageSize = 10,
Guid? UserId = null,
- string? Email = null)
+ string? Email = null,
+ string? SearchTerm = null,
+ string? Role = null)
: IRequest>>
{
public const int DefaultPageSize = 10;
public const int MaxPageSize = 100;
+
+ public const int MaxSearchTermLength = 256;
}
///
@@ -60,7 +64,9 @@ public async Task>> Handle(
var membershipQuery = new GetActiveTenantMembershipsForDirectoryQueryObject(
currentTenant.Id,
request.UserId is null ? null : new UserId(request.UserId.Value),
- request.Email)
+ request.Email,
+ request.SearchTerm,
+ request.Role)
.Apply(membershipRepository.Query());
var totalCount = await membershipQuery.CountAsync(cancellationToken);
diff --git a/src/GovUK.Dfe.FlexForms.Application/Users/Queries/GetTenantUsersQueryValidator.cs b/src/GovUK.Dfe.FlexForms.Application/Users/Queries/GetTenantUsersQueryValidator.cs
index b2f8cf05..fadf3c10 100644
--- a/src/GovUK.Dfe.FlexForms.Application/Users/Queries/GetTenantUsersQueryValidator.cs
+++ b/src/GovUK.Dfe.FlexForms.Application/Users/Queries/GetTenantUsersQueryValidator.cs
@@ -17,5 +17,13 @@ public GetTenantUsersQueryValidator()
RuleFor(x => x.Email!)
.EmailAddress();
});
+
+ RuleFor(x => x.SearchTerm!)
+ .MaximumLength(GetTenantUsersQuery.MaxSearchTermLength)
+ .When(x => !string.IsNullOrWhiteSpace(x.SearchTerm));
+
+ RuleFor(x => x.Role!)
+ .MaximumLength(50)
+ .When(x => !string.IsNullOrWhiteSpace(x.Role));
}
}
diff --git a/src/GovUK.Dfe.FlexForms.Infrastructure/Database/ExternalApplicationsContext.cs b/src/GovUK.Dfe.FlexForms.Infrastructure/Database/ExternalApplicationsContext.cs
index a96600f0..6bce9c28 100644
--- a/src/GovUK.Dfe.FlexForms.Infrastructure/Database/ExternalApplicationsContext.cs
+++ b/src/GovUK.Dfe.FlexForms.Infrastructure/Database/ExternalApplicationsContext.cs
@@ -277,6 +277,10 @@ private static void ConfigureTenantMembership(EntityTypeBuilder e.UserId)
.HasDatabaseName("IX_TenantMemberships_UserId");
+ // Serves the User Manager directory listing, which always scopes to an active tenant
+ // membership and optionally narrows to a single role.
+ b.HasIndex(e => new { e.TenantId, e.IsActive, e.RoleId })
+ .HasDatabaseName("IX_TenantMemberships_TenantId_IsActive_RoleId");
if (useTemporal)
{
@@ -383,6 +387,9 @@ private static void ConfigureUser(EntityTypeBuilder b, bool useTemporal)
.IsUnicode(false);
b.HasIndex(u => u.ExternalProviderId).IsUnique();
b.HasIndex(e => e.Email).IsUnique();
+ // The tenant user directory orders by name, so keep a sorted copy available.
+ b.HasIndex(e => e.Name)
+ .HasDatabaseName("IX_Users_Name");
b.HasOne(e => e.Role)
.WithMany()
.HasForeignKey(e => e.RoleId);
diff --git a/src/GovUK.Dfe.FlexForms.Infrastructure/Migrations/20260910082328_AddTenantUserDirectoryFilterIndexes.Designer.cs b/src/GovUK.Dfe.FlexForms.Infrastructure/Migrations/20260910082328_AddTenantUserDirectoryFilterIndexes.Designer.cs
new file mode 100644
index 00000000..88d57e81
--- /dev/null
+++ b/src/GovUK.Dfe.FlexForms.Infrastructure/Migrations/20260910082328_AddTenantUserDirectoryFilterIndexes.Designer.cs
@@ -0,0 +1,1035 @@
+//
+using System;
+using GovUK.Dfe.FlexForms.Infrastructure.Database;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace GovUK.Dfe.FlexForms.Infrastructure.Migrations
+{
+ [DbContext(typeof(ExternalApplicationsContext))]
+ [Migration("20260910082328_AddTenantUserDirectoryFilterIndexes")]
+ partial class AddTenantUserDirectoryFilterIndexes
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "10.0.11")
+ .HasAnnotation("Relational:MaxIdentifierLength", 128);
+
+ SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
+
+ modelBuilder.Entity("GovUK.Dfe.FlexForms.Domain.Entities.Application", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier")
+ .HasColumnName("ApplicationId");
+
+ b.Property("ApplicationReference")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("nvarchar(20)")
+ .HasColumnName("ApplicationReference");
+
+ b.Property("CreatedBy")
+ .HasColumnType("uniqueidentifier")
+ .HasColumnName("CreatedBy");
+
+ b.Property("CreatedOn")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("datetime2")
+ .HasColumnName("CreatedOn")
+ .HasDefaultValueSql("GETDATE()");
+
+ b.Property("DeletedBy")
+ .HasColumnType("uniqueidentifier")
+ .HasColumnName("DeletedBy");
+
+ b.Property("DeletedOn")
+ .HasColumnType("datetime2")
+ .HasColumnName("DeletedOn");
+
+ b.Property("LastModifiedBy")
+ .HasColumnType("uniqueidentifier")
+ .HasColumnName("LastModifiedBy");
+
+ b.Property("LastModifiedOn")
+ .HasColumnType("datetime2")
+ .HasColumnName("LastModifiedOn");
+
+ b.Property("PeriodEnd")
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType("datetime2")
+ .HasColumnName("PeriodEnd");
+
+ b.Property("PeriodStart")
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType("datetime2")
+ .HasColumnName("PeriodStart");
+
+ b.Property("Status")
+ .HasColumnType("int")
+ .HasColumnName("Status");
+
+ b.Property("TemplateVersionId")
+ .HasColumnType("uniqueidentifier")
+ .HasColumnName("TemplateVersionId");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ApplicationReference")
+ .IsUnique()
+ .HasDatabaseName("IX_Applications_ApplicationReference");
+
+ b.HasIndex("CreatedBy");
+
+ b.HasIndex("CreatedOn")
+ .HasDatabaseName("IX_Applications_CreatedOn");
+
+ b.HasIndex("DeletedBy");
+
+ b.HasIndex("LastModifiedBy");
+
+ b.HasIndex("TemplateVersionId")
+ .HasDatabaseName("IX_Applications_TemplateVersionId");
+
+ b.HasIndex("Status", "LastModifiedOn")
+ .HasDatabaseName("IX_Applications_Status_LastModifiedOn");
+
+ b.ToTable("Applications", "ea");
+
+ b.ToTable(tb => tb.IsTemporal(ttb =>
+ {
+ ttb.UseHistoryTable("History_Applications", "ea");
+ ttb
+ .HasPeriodStart("PeriodStart")
+ .HasColumnName("PeriodStart");
+ ttb
+ .HasPeriodEnd("PeriodEnd")
+ .HasColumnName("PeriodEnd");
+ }));
+ });
+
+ modelBuilder.Entity("GovUK.Dfe.FlexForms.Domain.Entities.ApplicationResponse", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uniqueidentifier")
+ .HasColumnName("ResponseId");
+
+ b.Property("ApplicationId")
+ .HasColumnType("uniqueidentifier")
+ .HasColumnName("ApplicationId");
+
+ b.Property("CreatedBy")
+ .HasColumnType("uniqueidentifier")
+ .HasColumnName("CreatedBy");
+
+ b.Property("CreatedOn")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("datetime2")
+ .HasColumnName("CreatedOn")
+ .HasDefaultValueSql("GETDATE()");
+
+ b.Property("LastModifiedBy")
+ .HasColumnType("uniqueidentifier")
+ .HasColumnName("LastModifiedBy");
+
+ b.Property("LastModifiedOn")
+ .HasColumnType("datetime2")
+ .HasColumnName("LastModifiedOn");
+
+ b.Property("ResponseBody")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)")
+ .HasColumnName("ResponseBody");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CreatedBy");
+
+ b.HasIndex("LastModifiedBy");
+
+ b.HasIndex("ApplicationId", "CreatedOn")
+ .IsDescending(false, true)
+ .HasDatabaseName("IX_ApplicationResponses_ApplicationId_CreatedOn");
+
+ b.ToTable("ApplicationResponses", "ea");
+ });
+
+ modelBuilder.Entity("GovUK.Dfe.FlexForms.Domain.Entities.CustomApplicationStatus", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uniqueidentifier")
+ .HasColumnName("CustomApplicationStatusId");
+
+ b.Property("ApplicationStatus")
+ .HasColumnType("int")
+ .HasColumnName("ApplicationStatus");
+
+ b.Property("CreatedBy")
+ .HasColumnType("uniqueidentifier")
+ .HasColumnName("CreatedBy");
+
+ b.Property("CreatedOn")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("datetime2")
+ .HasColumnName("CreatedOn")
+ .HasDefaultValueSql("GETDATE()");
+
+ b.Property("Label")
+ .HasMaxLength(200)
+ .HasColumnType("nvarchar(200)")
+ .HasColumnName("Label");
+
+ b.Property("PeriodEnd")
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType("datetime2")
+ .HasColumnName("PeriodEnd");
+
+ b.Property("PeriodStart")
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType("datetime2")
+ .HasColumnName("PeriodStart");
+
+ b.Property("TemplateId")
+ .HasColumnType("uniqueidentifier")
+ .HasColumnName("TemplateId");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CreatedBy");
+
+ b.HasIndex("TemplateId", "ApplicationStatus")
+ .IsUnique()
+ .HasDatabaseName("IX_CustomApplicationStatuses_TemplateId_ApplicationStatus");
+
+ b.ToTable("CustomApplicationStatuses", "ea");
+
+ b.ToTable(tb => tb.IsTemporal(ttb =>
+ {
+ ttb.UseHistoryTable("History_CustomApplicationStatuses", "ea");
+ ttb
+ .HasPeriodStart("PeriodStart")
+ .HasColumnName("PeriodStart");
+ ttb
+ .HasPeriodEnd("PeriodEnd")
+ .HasColumnName("PeriodEnd");
+ }));
+ });
+
+ modelBuilder.Entity("GovUK.Dfe.FlexForms.Domain.Entities.File", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier")
+ .HasColumnName("FileId");
+
+ b.Property("ApplicationId")
+ .HasColumnType("uniqueidentifier")
+ .HasColumnName("ApplicationId");
+
+ b.Property("Description")
+ .HasMaxLength(1000)
+ .HasColumnType("nvarchar(1000)")
+ .HasColumnName("Description");
+
+ b.Property("FileName")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("nvarchar(255)")
+ .HasColumnName("FileName");
+
+ b.Property("FileSize")
+ .HasColumnType("bigint")
+ .HasColumnName("FileSize");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("nvarchar(255)")
+ .HasColumnName("Name");
+
+ b.Property("OriginalFileName")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("nvarchar(255)")
+ .HasColumnName("OriginalFileName");
+
+ b.Property("Path")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("nvarchar(255)")
+ .HasColumnName("Path");
+
+ b.Property("UploadedBy")
+ .HasColumnType("uniqueidentifier")
+ .HasColumnName("UploadedBy");
+
+ b.Property("UploadedOn")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("datetime2")
+ .HasColumnName("UploadedOn")
+ .HasDefaultValueSql("GETDATE()");
+
+ b.Property("ValidatedOn")
+ .HasColumnType("datetime2")
+ .HasColumnName("ValidatedOn");
+
+ b.Property("ValidationMessage")
+ .HasMaxLength(1000)
+ .HasColumnType("nvarchar(1000)")
+ .HasColumnName("ValidationMessage");
+
+ b.Property("ValidationSource")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)")
+ .HasColumnName("ValidationSource");
+
+ b.Property("ValidationStatus")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("nvarchar(20)")
+ .HasColumnName("ValidationStatus");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ApplicationId")
+ .HasDatabaseName("IX_Files_ApplicationId");
+
+ b.HasIndex("UploadedBy");
+
+ b.HasIndex("ApplicationId", "FileName")
+ .HasDatabaseName("IX_Files_ApplicationId_FileName");
+
+ b.HasIndex("Path", "FileName")
+ .HasDatabaseName("IX_Files_Path_FileName");
+
+ b.ToTable("Files", "ea");
+ });
+
+ modelBuilder.Entity("GovUK.Dfe.FlexForms.Domain.Entities.Permission", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uniqueidentifier")
+ .HasColumnName("PermissionId");
+
+ b.Property("AccessType")
+ .HasColumnType("tinyint")
+ .HasColumnName("AccessType");
+
+ b.Property("ApplicationId")
+ .HasColumnType("uniqueidentifier")
+ .HasColumnName("ApplicationId");
+
+ b.Property("GrantedBy")
+ .HasColumnType("uniqueidentifier")
+ .HasColumnName("GrantedBy");
+
+ b.Property("GrantedOn")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("datetime2")
+ .HasColumnName("GrantedOn")
+ .HasDefaultValueSql("GETDATE()");
+
+ b.Property("ResourceKey")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)")
+ .HasColumnName("ResourceKey");
+
+ b.Property("ResourceType")
+ .HasColumnType("tinyint")
+ .HasColumnName("ResourceType");
+
+ b.Property("UserId")
+ .HasColumnType("uniqueidentifier")
+ .HasColumnName("UserId");
+
+ b.HasKey("Id");
+
+ b.HasIndex("GrantedBy");
+
+ b.HasIndex("ApplicationId", "ResourceType")
+ .HasDatabaseName("IX_Permissions_ApplicationId_ResourceType");
+
+ b.HasIndex("UserId", "ResourceType", "ApplicationId")
+ .HasDatabaseName("IX_Permissions_UserId_ResourceType_ApplicationId");
+
+ b.ToTable("Permissions", "ea");
+ });
+
+ modelBuilder.Entity("GovUK.Dfe.FlexForms.Domain.Entities.Role", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier")
+ .HasColumnName("RoleId");
+
+ b.Property("IsSystem")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bit")
+ .HasDefaultValue(false)
+ .HasColumnName("IsSystem");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("nvarchar(50)")
+ .HasColumnName("Name");
+
+ b.Property("PeriodEnd")
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType("datetime2")
+ .HasColumnName("PeriodEnd");
+
+ b.Property("PeriodStart")
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType("datetime2")
+ .HasColumnName("PeriodStart");
+
+ b.Property("TenantId")
+ .HasColumnType("uniqueidentifier")
+ .HasColumnName("TenantId");
+
+ b.HasKey("Id");
+
+ b.HasIndex("TenantId")
+ .HasDatabaseName("IX_Roles_TenantId");
+
+ b.HasIndex("TenantId", "Name")
+ .IsUnique()
+ .HasDatabaseName("IX_Roles_TenantId_Name")
+ .HasFilter("[TenantId] IS NOT NULL");
+
+ b.ToTable("Roles", "ea");
+
+ b.ToTable(tb => tb.IsTemporal(ttb =>
+ {
+ ttb.UseHistoryTable("History_Roles", "ea");
+ ttb
+ .HasPeriodStart("PeriodStart")
+ .HasColumnName("PeriodStart");
+ ttb
+ .HasPeriodEnd("PeriodEnd")
+ .HasColumnName("PeriodEnd");
+ }));
+ });
+
+ modelBuilder.Entity("GovUK.Dfe.FlexForms.Domain.Entities.RolePermission", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uniqueidentifier")
+ .HasColumnName("RolePermissionId");
+
+ b.Property("AccessType")
+ .HasColumnType("int")
+ .HasColumnName("AccessType");
+
+ b.Property("CreatedOn")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("datetime2")
+ .HasColumnName("CreatedOn")
+ .HasDefaultValueSql("GETDATE()");
+
+ b.Property("ResourceKey")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)")
+ .HasColumnName("ResourceKey");
+
+ b.Property("ResourceType")
+ .HasColumnType("int")
+ .HasColumnName("ResourceType");
+
+ b.Property("RoleId")
+ .HasColumnType("uniqueidentifier")
+ .HasColumnName("RoleId");
+
+ b.HasKey("Id");
+
+ b.HasIndex("RoleId", "ResourceType", "ResourceKey", "AccessType")
+ .IsUnique()
+ .HasDatabaseName("IX_RolePermissions_Role_Resource_Access");
+
+ b.ToTable("RolePermissions", "ea");
+ });
+
+ modelBuilder.Entity("GovUK.Dfe.FlexForms.Domain.Entities.Template", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier")
+ .HasColumnName("TemplateId");
+
+ b.Property("CreatedBy")
+ .HasColumnType("uniqueidentifier")
+ .HasColumnName("CreatedBy");
+
+ b.Property("CreatedOn")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("datetime2")
+ .HasColumnName("CreatedOn")
+ .HasDefaultValueSql("GETDATE()");
+
+ b.Property("IsLive")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bit")
+ .HasDefaultValue(false)
+ .HasColumnName("IsLive");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)")
+ .HasColumnName("Name");
+
+ b.Property("PeriodEnd")
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType("datetime2")
+ .HasColumnName("PeriodEnd");
+
+ b.Property("PeriodStart")
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType("datetime2")
+ .HasColumnName("PeriodStart");
+
+ b.Property("TenantId")
+ .HasColumnType("uniqueidentifier")
+ .HasColumnName("TenantId");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CreatedBy");
+
+ b.HasIndex("TenantId");
+
+ b.HasIndex("TenantId", "Name")
+ .IsUnique()
+ .HasDatabaseName("IX_Templates_TenantId_Name")
+ .HasFilter("[TenantId] IS NOT NULL");
+
+ b.ToTable("Templates", "ea");
+
+ b.ToTable(tb => tb.IsTemporal(ttb =>
+ {
+ ttb.UseHistoryTable("History_Templates", "ea");
+ ttb
+ .HasPeriodStart("PeriodStart")
+ .HasColumnName("PeriodStart");
+ ttb
+ .HasPeriodEnd("PeriodEnd")
+ .HasColumnName("PeriodEnd");
+ }));
+ });
+
+ modelBuilder.Entity("GovUK.Dfe.FlexForms.Domain.Entities.TemplateVersion", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uniqueidentifier")
+ .HasColumnName("TemplateVersionId");
+
+ b.Property("CreatedBy")
+ .HasColumnType("uniqueidentifier")
+ .HasColumnName("CreatedBy");
+
+ b.Property("CreatedOn")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("datetime2")
+ .HasColumnName("CreatedOn")
+ .HasDefaultValueSql("GETDATE()");
+
+ b.Property("JsonSchema")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)")
+ .HasColumnName("JsonSchema");
+
+ b.Property("LastModifiedBy")
+ .HasColumnType("uniqueidentifier")
+ .HasColumnName("LastModifiedBy");
+
+ b.Property("LastModifiedOn")
+ .HasColumnType("datetime2")
+ .HasColumnName("LastModifiedOn");
+
+ b.Property("TemplateId")
+ .HasColumnType("uniqueidentifier")
+ .HasColumnName("TemplateId");
+
+ b.Property("VersionNumber")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("nvarchar(50)")
+ .HasColumnName("VersionNumber");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CreatedBy");
+
+ b.HasIndex("LastModifiedBy");
+
+ b.HasIndex("TemplateId", "CreatedOn")
+ .IsDescending(false, true)
+ .HasDatabaseName("IX_TemplateVersions_TemplateId_CreatedOn");
+
+ b.ToTable("TemplateVersions", "ea");
+ });
+
+ modelBuilder.Entity("GovUK.Dfe.FlexForms.Domain.Entities.TenantAccessAudit", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("Action")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("ActorEmail")
+ .IsRequired()
+ .HasMaxLength(320)
+ .HasColumnType("nvarchar(320)");
+
+ b.Property("ActorUserId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("Details")
+ .HasMaxLength(1024)
+ .HasColumnType("nvarchar(1024)");
+
+ b.Property("OccurredAtUtc")
+ .HasColumnType("datetime2");
+
+ b.Property("RoleName")
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)");
+
+ b.Property("SubjectEmail")
+ .IsRequired()
+ .HasMaxLength(320)
+ .HasColumnType("nvarchar(320)");
+
+ b.Property("SubjectUserId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("TenantId")
+ .HasColumnType("uniqueidentifier");
+
+ b.HasKey("Id");
+
+ b.HasIndex("TenantId", "OccurredAtUtc")
+ .HasDatabaseName("IX_TenantAccessAudits_TenantId_OccurredAtUtc");
+
+ b.HasIndex("TenantId", "SubjectEmail")
+ .HasDatabaseName("IX_TenantAccessAudits_TenantId_SubjectEmail");
+
+ b.ToTable("TenantAccessAudits", "ea");
+ });
+
+ modelBuilder.Entity("GovUK.Dfe.FlexForms.Domain.Entities.TenantMembership", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uniqueidentifier")
+ .HasColumnName("TenantMembershipId");
+
+ b.Property("CreatedOn")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("datetime2")
+ .HasColumnName("CreatedOn")
+ .HasDefaultValueSql("GETDATE()");
+
+ b.Property("IsActive")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bit")
+ .HasDefaultValue(true)
+ .HasColumnName("IsActive");
+
+ b.Property("LastModifiedOn")
+ .HasColumnType("datetime2")
+ .HasColumnName("LastModifiedOn");
+
+ b.Property("PeriodEnd")
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType("datetime2")
+ .HasColumnName("PeriodEnd");
+
+ b.Property("PeriodStart")
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType("datetime2")
+ .HasColumnName("PeriodStart");
+
+ b.Property("RoleId")
+ .HasColumnType("uniqueidentifier")
+ .HasColumnName("RoleId");
+
+ b.Property("TenantId")
+ .HasColumnType("uniqueidentifier")
+ .HasColumnName("TenantId");
+
+ b.Property("UserId")
+ .HasColumnType("uniqueidentifier")
+ .HasColumnName("UserId");
+
+ b.HasKey("Id");
+
+ b.HasIndex("RoleId");
+
+ b.HasIndex("UserId")
+ .HasDatabaseName("IX_TenantMemberships_UserId");
+
+ b.HasIndex("TenantId", "UserId")
+ .IsUnique()
+ .HasDatabaseName("IX_TenantMemberships_TenantId_UserId");
+
+ b.HasIndex("TenantId", "IsActive", "RoleId")
+ .HasDatabaseName("IX_TenantMemberships_TenantId_IsActive_RoleId");
+
+ b.ToTable("TenantMemberships", "ea");
+
+ b.ToTable(tb => tb.IsTemporal(ttb =>
+ {
+ ttb.UseHistoryTable("History_TenantMemberships", "ea");
+ ttb
+ .HasPeriodStart("PeriodStart")
+ .HasColumnName("PeriodStart");
+ ttb
+ .HasPeriodEnd("PeriodEnd")
+ .HasColumnName("PeriodEnd");
+ }));
+ });
+
+ modelBuilder.Entity("GovUK.Dfe.FlexForms.Domain.Entities.User", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier")
+ .HasColumnName("UserId");
+
+ b.Property("CreatedBy")
+ .HasColumnType("uniqueidentifier")
+ .HasColumnName("CreatedBy");
+
+ b.Property("CreatedOn")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("datetime2")
+ .HasColumnName("CreatedOn")
+ .HasDefaultValueSql("GETDATE()");
+
+ b.Property("Email")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)")
+ .HasColumnName("Email");
+
+ b.Property("ExternalProviderId")
+ .HasMaxLength(100)
+ .IsUnicode(false)
+ .HasColumnType("varchar(100)");
+
+ b.Property("LastModifiedBy")
+ .HasColumnType("uniqueidentifier")
+ .HasColumnName("LastModifiedBy");
+
+ b.Property("LastModifiedOn")
+ .HasColumnType("datetime2")
+ .HasColumnName("LastModifiedOn");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)")
+ .HasColumnName("Name");
+
+ b.Property("PeriodEnd")
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType("datetime2")
+ .HasColumnName("PeriodEnd");
+
+ b.Property("PeriodStart")
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType("datetime2")
+ .HasColumnName("PeriodStart");
+
+ b.Property("RoleId")
+ .HasColumnType("uniqueidentifier")
+ .HasColumnName("RoleId");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CreatedBy");
+
+ b.HasIndex("Email")
+ .IsUnique();
+
+ b.HasIndex("ExternalProviderId")
+ .IsUnique()
+ .HasFilter("[ExternalProviderId] IS NOT NULL");
+
+ b.HasIndex("LastModifiedBy");
+
+ b.HasIndex("Name")
+ .HasDatabaseName("IX_Users_Name");
+
+ b.HasIndex("RoleId");
+
+ b.ToTable("Users", "ea");
+
+ b.ToTable(tb => tb.IsTemporal(ttb =>
+ {
+ ttb.UseHistoryTable("History_Users", "ea");
+ ttb
+ .HasPeriodStart("PeriodStart")
+ .HasColumnName("PeriodStart");
+ ttb
+ .HasPeriodEnd("PeriodEnd")
+ .HasColumnName("PeriodEnd");
+ }));
+ });
+
+ modelBuilder.Entity("GovUK.Dfe.FlexForms.Domain.Entities.Application", b =>
+ {
+ b.HasOne("GovUK.Dfe.FlexForms.Domain.Entities.User", "CreatedByUser")
+ .WithMany()
+ .HasForeignKey("CreatedBy")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.HasOne("GovUK.Dfe.FlexForms.Domain.Entities.User", "DeletedByUser")
+ .WithMany()
+ .HasForeignKey("DeletedBy")
+ .OnDelete(DeleteBehavior.Restrict);
+
+ b.HasOne("GovUK.Dfe.FlexForms.Domain.Entities.User", "LastModifiedByUser")
+ .WithMany()
+ .HasForeignKey("LastModifiedBy")
+ .OnDelete(DeleteBehavior.Restrict);
+
+ b.HasOne("GovUK.Dfe.FlexForms.Domain.Entities.TemplateVersion", "TemplateVersion")
+ .WithMany()
+ .HasForeignKey("TemplateVersionId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("CreatedByUser");
+
+ b.Navigation("DeletedByUser");
+
+ b.Navigation("LastModifiedByUser");
+
+ b.Navigation("TemplateVersion");
+ });
+
+ modelBuilder.Entity("GovUK.Dfe.FlexForms.Domain.Entities.ApplicationResponse", b =>
+ {
+ b.HasOne("GovUK.Dfe.FlexForms.Domain.Entities.Application", "Application")
+ .WithMany("Responses")
+ .HasForeignKey("ApplicationId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("GovUK.Dfe.FlexForms.Domain.Entities.User", "CreatedByUser")
+ .WithMany()
+ .HasForeignKey("CreatedBy")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.HasOne("GovUK.Dfe.FlexForms.Domain.Entities.User", "LastModifiedByUser")
+ .WithMany()
+ .HasForeignKey("LastModifiedBy")
+ .OnDelete(DeleteBehavior.Restrict);
+
+ b.Navigation("Application");
+
+ b.Navigation("CreatedByUser");
+
+ b.Navigation("LastModifiedByUser");
+ });
+
+ modelBuilder.Entity("GovUK.Dfe.FlexForms.Domain.Entities.CustomApplicationStatus", b =>
+ {
+ b.HasOne("GovUK.Dfe.FlexForms.Domain.Entities.User", "CreatedByUser")
+ .WithMany()
+ .HasForeignKey("CreatedBy")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.HasOne("GovUK.Dfe.FlexForms.Domain.Entities.Template", "Template")
+ .WithMany()
+ .HasForeignKey("TemplateId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("CreatedByUser");
+
+ b.Navigation("Template");
+ });
+
+ modelBuilder.Entity("GovUK.Dfe.FlexForms.Domain.Entities.File", b =>
+ {
+ b.HasOne("GovUK.Dfe.FlexForms.Domain.Entities.Application", "Application")
+ .WithMany("Files")
+ .HasForeignKey("ApplicationId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("GovUK.Dfe.FlexForms.Domain.Entities.User", "UploadedByUser")
+ .WithMany("Files")
+ .HasForeignKey("UploadedBy")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.Navigation("Application");
+
+ b.Navigation("UploadedByUser");
+ });
+
+ modelBuilder.Entity("GovUK.Dfe.FlexForms.Domain.Entities.Permission", b =>
+ {
+ b.HasOne("GovUK.Dfe.FlexForms.Domain.Entities.Application", "Application")
+ .WithMany()
+ .HasForeignKey("ApplicationId");
+
+ b.HasOne("GovUK.Dfe.FlexForms.Domain.Entities.User", "GrantedByUser")
+ .WithMany()
+ .HasForeignKey("GrantedBy")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.HasOne("GovUK.Dfe.FlexForms.Domain.Entities.User", "User")
+ .WithMany("Permissions")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Application");
+
+ b.Navigation("GrantedByUser");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("GovUK.Dfe.FlexForms.Domain.Entities.RolePermission", b =>
+ {
+ b.HasOne("GovUK.Dfe.FlexForms.Domain.Entities.Role", "Role")
+ .WithMany("Permissions")
+ .HasForeignKey("RoleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Role");
+ });
+
+ modelBuilder.Entity("GovUK.Dfe.FlexForms.Domain.Entities.Template", b =>
+ {
+ b.HasOne("GovUK.Dfe.FlexForms.Domain.Entities.User", "CreatedByUser")
+ .WithMany()
+ .HasForeignKey("CreatedBy")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.Navigation("CreatedByUser");
+ });
+
+ modelBuilder.Entity("GovUK.Dfe.FlexForms.Domain.Entities.TemplateVersion", b =>
+ {
+ b.HasOne("GovUK.Dfe.FlexForms.Domain.Entities.User", "CreatedByUser")
+ .WithMany()
+ .HasForeignKey("CreatedBy")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.HasOne("GovUK.Dfe.FlexForms.Domain.Entities.User", "LastModifiedByUser")
+ .WithMany()
+ .HasForeignKey("LastModifiedBy")
+ .OnDelete(DeleteBehavior.Restrict);
+
+ b.HasOne("GovUK.Dfe.FlexForms.Domain.Entities.Template", "Template")
+ .WithMany("TemplateVersions")
+ .HasForeignKey("TemplateId")
+ .OnDelete(DeleteBehavior.NoAction)
+ .IsRequired();
+
+ b.Navigation("CreatedByUser");
+
+ b.Navigation("LastModifiedByUser");
+
+ b.Navigation("Template");
+ });
+
+ modelBuilder.Entity("GovUK.Dfe.FlexForms.Domain.Entities.TenantMembership", b =>
+ {
+ b.HasOne("GovUK.Dfe.FlexForms.Domain.Entities.Role", "Role")
+ .WithMany()
+ .HasForeignKey("RoleId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.HasOne("GovUK.Dfe.FlexForms.Domain.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Role");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("GovUK.Dfe.FlexForms.Domain.Entities.User", b =>
+ {
+ b.HasOne("GovUK.Dfe.FlexForms.Domain.Entities.User", "CreatedByUser")
+ .WithMany()
+ .HasForeignKey("CreatedBy")
+ .OnDelete(DeleteBehavior.Restrict);
+
+ b.HasOne("GovUK.Dfe.FlexForms.Domain.Entities.User", "LastModifiedByUser")
+ .WithMany()
+ .HasForeignKey("LastModifiedBy")
+ .OnDelete(DeleteBehavior.Restrict);
+
+ b.HasOne("GovUK.Dfe.FlexForms.Domain.Entities.Role", "Role")
+ .WithMany()
+ .HasForeignKey("RoleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("CreatedByUser");
+
+ b.Navigation("LastModifiedByUser");
+
+ b.Navigation("Role");
+ });
+
+ modelBuilder.Entity("GovUK.Dfe.FlexForms.Domain.Entities.Application", b =>
+ {
+ b.Navigation("Files");
+
+ b.Navigation("Responses");
+ });
+
+ modelBuilder.Entity("GovUK.Dfe.FlexForms.Domain.Entities.Role", b =>
+ {
+ b.Navigation("Permissions");
+ });
+
+ modelBuilder.Entity("GovUK.Dfe.FlexForms.Domain.Entities.Template", b =>
+ {
+ b.Navigation("TemplateVersions");
+ });
+
+ modelBuilder.Entity("GovUK.Dfe.FlexForms.Domain.Entities.User", b =>
+ {
+ b.Navigation("Files");
+
+ b.Navigation("Permissions");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/src/GovUK.Dfe.FlexForms.Infrastructure/Migrations/20260910082328_AddTenantUserDirectoryFilterIndexes.cs b/src/GovUK.Dfe.FlexForms.Infrastructure/Migrations/20260910082328_AddTenantUserDirectoryFilterIndexes.cs
new file mode 100644
index 00000000..e2f111eb
--- /dev/null
+++ b/src/GovUK.Dfe.FlexForms.Infrastructure/Migrations/20260910082328_AddTenantUserDirectoryFilterIndexes.cs
@@ -0,0 +1,40 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace GovUK.Dfe.FlexForms.Infrastructure.Migrations
+{
+ ///
+ public partial class AddTenantUserDirectoryFilterIndexes : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateIndex(
+ name: "IX_Users_Name",
+ schema: "ea",
+ table: "Users",
+ column: "Name");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_TenantMemberships_TenantId_IsActive_RoleId",
+ schema: "ea",
+ table: "TenantMemberships",
+ columns: new[] { "TenantId", "IsActive", "RoleId" });
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropIndex(
+ name: "IX_Users_Name",
+ schema: "ea",
+ table: "Users");
+
+ migrationBuilder.DropIndex(
+ name: "IX_TenantMemberships_TenantId_IsActive_RoleId",
+ schema: "ea",
+ table: "TenantMemberships");
+ }
+ }
+}
diff --git a/src/GovUK.Dfe.FlexForms.Infrastructure/Migrations/ExternalApplicationsContextModelSnapshot.cs b/src/GovUK.Dfe.FlexForms.Infrastructure/Migrations/ExternalApplicationsContextModelSnapshot.cs
index af9ceb19..c6dbfa9b 100644
--- a/src/GovUK.Dfe.FlexForms.Infrastructure/Migrations/ExternalApplicationsContextModelSnapshot.cs
+++ b/src/GovUK.Dfe.FlexForms.Infrastructure/Migrations/ExternalApplicationsContextModelSnapshot.cs
@@ -684,6 +684,9 @@ protected override void BuildModel(ModelBuilder modelBuilder)
.IsUnique()
.HasDatabaseName("IX_TenantMemberships_TenantId_UserId");
+ b.HasIndex("TenantId", "IsActive", "RoleId")
+ .HasDatabaseName("IX_TenantMemberships_TenantId_IsActive_RoleId");
+
b.ToTable("TenantMemberships", "ea");
b.ToTable(tb => tb.IsTemporal(ttb =>
@@ -767,6 +770,9 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.HasIndex("LastModifiedBy");
+ b.HasIndex("Name")
+ .HasDatabaseName("IX_Users_Name");
+
b.HasIndex("RoleId");
b.ToTable("Users", "ea");
diff --git a/src/Tests/GovUK.Dfe.FlexForms.Application.Tests/QueryHandlers/Users/GetTenantUsersQueryHandlerTests.cs b/src/Tests/GovUK.Dfe.FlexForms.Application.Tests/QueryHandlers/Users/GetTenantUsersQueryHandlerTests.cs
index 47ccc554..79336a8b 100644
--- a/src/Tests/GovUK.Dfe.FlexForms.Application.Tests/QueryHandlers/Users/GetTenantUsersQueryHandlerTests.cs
+++ b/src/Tests/GovUK.Dfe.FlexForms.Application.Tests/QueryHandlers/Users/GetTenantUsersQueryHandlerTests.cs
@@ -163,6 +163,62 @@ public async Task Handle_ShouldFilterByUserId()
Assert.Equal(1, result.Value.TotalCount);
}
+ [Fact]
+ public async Task Handle_ShouldFilterBySearchTerm_AndPageTheFilteredSetOnly()
+ {
+ var role = Role.CreateForTenant(TenantId, RoleNames.User, true);
+ var matches = Enumerable.Range(1, 3)
+ .Select(i => CreateUser($"Brown {i}", $"brown{i}@example.test", role.Id!))
+ .ToList();
+ var nonMatches = Enumerable.Range(1, 9)
+ .Select(i => CreateUser($"Singh {i}", $"singh{i}@example.test", role.Id!))
+ .ToList();
+
+ var membershipRepository = Substitute.For>();
+ membershipRepository.Query().Returns(matches.Concat(nonMatches)
+ .Select(u => CreateMembership(TenantId, u, role))
+ .AsQueryable()
+ .BuildMock());
+
+ var handler = CreateHandler(membershipRepository: membershipRepository);
+
+ var result = await handler.Handle(
+ new GetTenantUsersQuery(PageNumber: 1, PageSize: 10, SearchTerm: "brown"),
+ CancellationToken.None);
+
+ Assert.True(result.IsSuccess);
+ Assert.Equal(3, result.Value!.TotalCount);
+ Assert.Equal(1, result.Value.TotalPages);
+ Assert.All(result.Value.Items, item => Assert.StartsWith("Brown", item.Name));
+ }
+
+ [Fact]
+ public async Task Handle_ShouldFilterByRole()
+ {
+ var adminRole = Role.CreateForTenant(TenantId, RoleNames.Admin, true);
+ var userRole = Role.CreateForTenant(TenantId, RoleNames.User, true);
+ var admin = CreateUser("Ada", "ada@example.test", adminRole.Id!);
+ var member = CreateUser("Bob", "bob@example.test", userRole.Id!);
+
+ var membershipRepository = Substitute.For>();
+ membershipRepository.Query().Returns(new[]
+ {
+ CreateMembership(TenantId, admin, adminRole),
+ CreateMembership(TenantId, member, userRole)
+ }.AsQueryable().BuildMock());
+
+ var handler = CreateHandler(membershipRepository: membershipRepository);
+
+ var result = await handler.Handle(
+ new GetTenantUsersQuery(PageNumber: 1, PageSize: 10, Role: RoleNames.Admin),
+ CancellationToken.None);
+
+ var item = Assert.Single(result.Value!.Items);
+ Assert.Equal(admin.Id!.Value, item.UserId);
+ Assert.Equal(RoleNames.Admin, item.Role);
+ Assert.Equal(1, result.Value.TotalCount);
+ }
+
private static GetTenantUsersQueryHandler CreateHandler(
IPermissionCheckerService? permissionChecker = null,
ITenantContextAccessor? tenantContext = null,
diff --git a/src/Tests/GovUK.Dfe.FlexForms.Application.Tests/QueryObjects/TenantMemberships/GetActiveTenantMembershipsForDirectoryQueryObjectTests.cs b/src/Tests/GovUK.Dfe.FlexForms.Application.Tests/QueryObjects/TenantMemberships/GetActiveTenantMembershipsForDirectoryQueryObjectTests.cs
index 91f2b7b3..c885269d 100644
--- a/src/Tests/GovUK.Dfe.FlexForms.Application.Tests/QueryObjects/TenantMemberships/GetActiveTenantMembershipsForDirectoryQueryObjectTests.cs
+++ b/src/Tests/GovUK.Dfe.FlexForms.Application.Tests/QueryObjects/TenantMemberships/GetActiveTenantMembershipsForDirectoryQueryObjectTests.cs
@@ -79,6 +79,76 @@ public void Apply_ShouldFilterByEmail_IgnoringCase()
Assert.Equal("keep@example.test", membership.User!.Email);
}
+ [Theory]
+ [InlineData("cat", "Catriona Brown")]
+ [InlineData("BROWN", "Catriona Brown")]
+ [InlineData("dev@", "Devi Rao")]
+ public void Apply_ShouldFilterBySearchTerm_AcrossNameAndEmail(string searchTerm, string expectedName)
+ {
+ var tenantId = Guid.NewGuid();
+ var role = Role.CreateForTenant(tenantId, RoleNames.User, true);
+ var catriona = CreateUser("Catriona Brown", "c.brown@example.test", role.Id!);
+ var devi = CreateUser("Devi Rao", "dev@example.test", role.Id!);
+
+ var result = new GetActiveTenantMembershipsForDirectoryQueryObject(tenantId, searchTerm: searchTerm)
+ .Apply(new[]
+ {
+ CreateMembership(tenantId, catriona, role, active: true),
+ CreateMembership(tenantId, devi, role, active: true)
+ }.AsQueryable().BuildMock())
+ .ToList();
+
+ var membership = Assert.Single(result);
+ Assert.Equal(expectedName, membership.User!.Name);
+ }
+
+ [Fact]
+ public void Apply_ShouldFilterByRoleName_IgnoringCase()
+ {
+ var tenantId = Guid.NewGuid();
+ var adminRole = Role.CreateForTenant(tenantId, RoleNames.Admin, true);
+ var userRole = Role.CreateForTenant(tenantId, RoleNames.User, true);
+ var admin = CreateUser("Admin Person", "admin@example.test", adminRole.Id!);
+ var member = CreateUser("Member Person", "member@example.test", userRole.Id!);
+
+ var result = new GetActiveTenantMembershipsForDirectoryQueryObject(tenantId, role: "aDmIn")
+ .Apply(new[]
+ {
+ CreateMembership(tenantId, admin, adminRole, active: true),
+ CreateMembership(tenantId, member, userRole, active: true)
+ }.AsQueryable().BuildMock())
+ .ToList();
+
+ var membership = Assert.Single(result);
+ Assert.Equal("admin@example.test", membership.User!.Email);
+ }
+
+ [Fact]
+ public void Apply_ShouldCombineSearchTermAndRoleFilters()
+ {
+ var tenantId = Guid.NewGuid();
+ var adminRole = Role.CreateForTenant(tenantId, RoleNames.Admin, true);
+ var userRole = Role.CreateForTenant(tenantId, RoleNames.User, true);
+ var matchingAdmin = CreateUser("Brown, Catriona", "catriona@example.test", adminRole.Id!);
+ var sameNameDifferentRole = CreateUser("Brown, Barry", "barry@example.test", userRole.Id!);
+ var sameRoleDifferentName = CreateUser("Singh, Priya", "priya@example.test", adminRole.Id!);
+
+ var result = new GetActiveTenantMembershipsForDirectoryQueryObject(
+ tenantId,
+ searchTerm: "brown",
+ role: RoleNames.Admin)
+ .Apply(new[]
+ {
+ CreateMembership(tenantId, matchingAdmin, adminRole, active: true),
+ CreateMembership(tenantId, sameNameDifferentRole, userRole, active: true),
+ CreateMembership(tenantId, sameRoleDifferentName, adminRole, active: true)
+ }.AsQueryable().BuildMock())
+ .ToList();
+
+ var membership = Assert.Single(result);
+ Assert.Equal("catriona@example.test", membership.User!.Email);
+ }
+
private static User CreateUser(string name, string email, RoleId roleId)
{
return new User(