Lens Integration¶
Verified by tests
GraphQLLensAttributeTests, GraphQLLensScopeTests — library CI run #31657041675 (2026-08-13)
The [GraphQLLens] attribute marks lens interfaces for GraphQL exposure, enabling automatic query generation with configurable filtering, sorting, and paging.
Basic Usage¶
Basic Usage
This generates a GraphQL query field named orders with full data operations support.
Attribute Properties¶
| Property | Type | Default | Description |
|---|---|---|---|
QueryName |
string? |
Pluralized model name (OrderReadModel → orders) |
GraphQL field name |
Scope |
GraphQLLensScopes |
None (uses system default) |
Which fields to expose |
EnableFiltering |
bool |
true |
Enable where argument |
EnableSorting |
bool |
true |
Enable order argument |
EnablePaging |
bool |
true |
Enable Relay-style paging |
EnableProjection |
bool |
true |
Enable field selection optimization |
DefaultPageSize |
int |
10 |
Default items per page |
MaxPageSize |
int |
100 |
Maximum items per page |
GraphQLLensScopes¶
Control which parts of PerspectiveRow<T> are exposed through the GraphQL schema. The scope determines which nested fields are available for querying.
The data Parameter¶
When you query a lens-backed GraphQL field, the results are wrapped in PerspectiveRow<TModel>, which contains:
data- The business model (your perspective's projection)metadata- Event sourcing metadata (eventType, eventId, timestamp, correlationId, causationId)scope- Security/tenancy context (tenantId, userId, organizationId, customerId, allowedPrincipals)- System fields - Infrastructure fields exposed at the top level of each row (id, version, createdAt, updatedAt) when the scope includes
SystemFields
The data field contains your actual business model that the perspective projects. For example, if your perspective projects to ProductReadModel, the data field will expose ProductReadModel's properties.
Scope Control¶
Control which parts of PerspectiveRow<T> are exposed:
Scope Control
[Flags]
public enum GraphQLLensScopes {
None = 0, // Use system default (WhizbangGraphQLOptions.DefaultScope)
Data = 1 << 0, // TModel properties
Metadata = 1 << 1, // EventType, EventId, Timestamp, CorrelationId, CausationId
Scope = 1 << 2, // TenantId, UserId, OrganizationId, CustomerId, AllowedPrincipals
SystemFields = 1 << 3, // Id, CreatedAt, UpdatedAt, Version
// Presets
DataOnly = Data,
NoData = Metadata | Scope | SystemFields,
All = Data | Metadata | Scope | SystemFields
}
Configuration Examples¶
Data Only (Default)¶
Expose only the business data:
Data Only (Default)
[GraphQLLens(QueryName = "products", Scope = GraphQLLensScopes.DataOnly)]
public interface IProductLens : ILensQuery<ProductReadModel> { }
With Metadata¶
Include event sourcing metadata:
With Metadata
[GraphQLLens(
QueryName = "auditLog",
Scope = GraphQLLensScopes.Data | GraphQLLensScopes.Metadata | GraphQLLensScopes.SystemFields)]
public interface IAuditLens : ILensQuery<AuditReadModel> { }
{
auditLog {
nodes {
id
version
data {
action
description
}
metadata {
eventType
correlationId
timestamp
}
}
}
}
Full Row (Admin View)¶
Expose everything including scope data:
Full Row (Admin View)
[GraphQLLens(QueryName = "adminOrders", Scope = GraphQLLensScopes.All)]
public interface IAdminOrderLens : ILensQuery<OrderReadModel> { }
{
adminOrders {
nodes {
id
version
createdAt
updatedAt
data {
customerName
status
}
metadata {
eventType
correlationId
}
scope {
tenantId
userId
}
}
}
}
Filter-Only (No Paging)¶
Disable paging for simple lists:
Filter-Only (No Paging)
[GraphQLLens(
QueryName = "statuses",
EnablePaging = false,
EnableSorting = false)]
public interface IStatusLens : ILensQuery<StatusReadModel> { }
Custom Page Sizes¶
Configure paging limits:
Custom Page Sizes
[GraphQLLens(
QueryName = "transactions",
DefaultPageSize = 50,
MaxPageSize = 500)]
public interface ITransactionLens : ILensQuery<TransactionReadModel> { }
Generated Schema¶
For a lens like:
Generated Schema
[GraphQLLens(QueryName = "orders")]
public interface IOrderLens : ILensQuery<OrderReadModel> { }
public record OrderReadModel {
public string CustomerName { get; init; }
public string Status { get; init; }
public decimal TotalAmount { get; init; }
}
The generated GraphQL schema includes:
type Query
type Query {
orders(
where: OrderFilterInput
order: [OrderSortInput!]
first: Int
after: String
last: Int
before: String
): OrdersConnection
}
type OrdersConnection {
nodes: [Order!]
edges: [OrderEdge!]
pageInfo: PageInfo!
# totalCount is only added when the resolver uses
# [UsePaging(IncludeTotalCount = true)] - not enabled by the generated resolvers
}
type Order {
id: UUID!
version: Int!
data: OrderData!
metadata: PerspectiveMetadata
scope: PerspectiveScope
createdAt: DateTime!
updatedAt: DateTime!
}
type OrderData {
customerName: String!
status: String!
totalAmount: Decimal!
}
input OrderFilterInput {
and: [OrderFilterInput!]
or: [OrderFilterInput!]
data: OrderDataFilterInput
id: UuidOperationFilterInput
version: IntOperationFilterInput
}
Multiple Lenses for Same Model¶
You can create multiple lenses for the same model with different configurations:
Multiple Lenses for Same Model
// Public API - data only
[GraphQLLens(QueryName = "orders", Scope = GraphQLLensScopes.DataOnly)]
public interface IOrderLens : ILensQuery<OrderReadModel> { }
// Admin API - full access
[GraphQLLens(QueryName = "adminOrders", Scope = GraphQLLensScopes.All)]
public interface IAdminOrderLens : ILensQuery<OrderReadModel> { }
// Audit API - metadata focus
[GraphQLLens(
QueryName = "orderAudit",
Scope = GraphQLLensScopes.Metadata | GraphQLLensScopes.SystemFields,
EnableFiltering = false)]
public interface IOrderAuditLens : ILensQuery<OrderReadModel> { }