# TraxQuery & TraxMutation Attributes

Trax uses two separate attributes to opt trains into the auto-generated GraphQL schema. Only trains decorated with one of these attributes get typed fields generated by the `TrainTypeModule`. Trains without either attribute are not exposed as GraphQL endpoints.

- **`[TraxQuery]`**: exposes the train as a query field under `query { discover { ... } }`
- **`[TraxMutation]`**: exposes the train as a mutation field under `mutation { dispatch { ... } }`

These are mutually exclusive: a train cannot be both a query and a mutation.

## TraxQuery Definition

```csharp
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
public class TraxQueryAttribute : Attribute
{
    public string? Name { get; init; }
    public string? Description { get; init; }
    public string? DeprecationReason { get; init; }
    public string? Namespace { get; init; }
}
```

Query trains always execute synchronously via `RunAsync`. There are no operation parameters; queries cannot be queued.

### Properties

| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `Name` | `string?` | `null` | Overrides the auto-derived GraphQL field name. When null, the name is derived by stripping the `I` prefix and `Train` suffix from the service type name. |
| `Description` | `string?` | `null` | Human-readable description that appears in the GraphQL schema documentation. |
| `DeprecationReason` | `string?` | `null` | Marks the generated field as deprecated in the schema. |
| `Namespace` | `string?` | `null` | Groups this field under a sub-namespace. When set, the field appears under `discover { namespace { field } }` instead of directly under `discover`. |

## TraxMutation Definition

```csharp
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
public class TraxMutationAttribute : Attribute
{
    public TraxMutationAttribute(params GraphQLOperation[] operations) { ... }

    public string? Name { get; init; }
    public string? Description { get; init; }
    public string? DeprecationReason { get; init; }
    public string? Namespace { get; init; }
    public GraphQLOperation Operations { get; }  // computed from constructor params
}
```

The constructor accepts `params GraphQLOperation[]`. When no operations are passed, both `Run` and `Queue` are enabled (the default). The `Operations` property is read-only and computed from the constructor arguments.

### Properties

| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `Name` | `string?` | `null` | Overrides the auto-derived GraphQL field name. When null, the name is derived by stripping the `I` prefix and `Train` suffix from the service type name. |
| `Description` | `string?` | `null` | Human-readable description that appears in the GraphQL schema documentation. |
| `DeprecationReason` | `string?` | `null` | Marks the generated fields as deprecated in the schema. |
| `Namespace` | `string?` | `null` | Groups this field under a sub-namespace. When set, the field appears under `dispatch { namespace { field } }` instead of directly under `dispatch`. |
| `Operations` | `GraphQLOperation` | `Run \| Queue` | Read-only. Flags value computed from constructor params. When both `Run` and `Queue` are set, a `mode` parameter is exposed; `Run` alone is synchronous-only; `Queue` alone is async-only. |

## GraphQLOperation Enum

```csharp
[Flags]
public enum GraphQLOperation
{
    Run = 1,    // Always runs synchronously (no mode param)
    Queue = 2,  // Always queues (no mode param, has priority)
}
```

When no operations are passed to the `TraxMutationAttribute` constructor, both `Run` and `Queue` are enabled. The generated mutation accepts an optional `mode: ExecutionMode` parameter (default `RUN`) and an optional `priority: Int`. To restrict a mutation to only one execution mode, pass just `GraphQLOperation.Run` or `GraphQLOperation.Queue` to the constructor.

## Input Type Requirements

Every train annotated with `[TraxQuery]` or `[TraxMutation]` must have a dedicated input record. `LanguageExt.Unit` is not allowed as an input type. Attempting to register a Unit-input train will throw `InvalidOperationException` at startup.

This requirement exists because:

1. The mediator bus routes by input type (`input.GetType()`), so `Unit` inputs would collide across trains
2. HotChocolate cannot create an `InputObjectType` for types with no properties

Empty records are valid. They provide a unique type for routing while the GraphQL layer skips the `input` argument:

```csharp
// Valid: empty record gives this train a unique input type
public record RefreshCacheInput;

[TraxMutation(GraphQLOperation.Run)]
public class RefreshCacheTrain : ServiceTrain<RefreshCacheInput, Unit>, IRefreshCacheTrain
{
    protected override Task<Either<Exception, Unit>> Junctions() =>
        Chain<RefreshCacheJunction>().Resolve();
}
```

When using `trax-cli` to generate trains from OpenAPI or GraphQL schemas, operations with no input parameters automatically get an empty input record.

Adding or removing the **last** property of an input record therefore changes the field's arity: the `input` argument appears or disappears. That is a breaking schema change for clients, and it does not look like one in the diff, because the record gained or lost a single property like any other.

## Naming Convention

When `Name` is null (the default), the field name is derived from the train's service interface:

1. Strip the `I` prefix
2. Strip the `Train` suffix
3. Use the result as the field name (camelCase, no prefix)

| Service Interface | Query Field | Mutation Field |
|-------------------|-------------|----------------|
| `ILookupPlayerTrain` | `lookupPlayer` | `lookupPlayer` |
| `IBanPlayerTrain` | `banPlayer` | `banPlayer` |

When `Name` is set, it's used directly: `[TraxMutation(Name = "BanUser")]` produces `banUser`.

Two exposed trains cannot share a name. `AddTraxGraphQL` refuses the host when two trains would produce the same field in the same place (both under `dispatch`, or both under `discover { players }`), or when two mutation trains would produce the same response type (`{Name}Response`, whose name is schema-wide even when the fields sit in different namespaces). The error names both trains' interfaces. Set `Name` on one of them: `Billing.ICreateOrderTrain` and `Shop.ICreateOrderTrain` both derive `CreateOrder`, so give them `Name = "BillingCreateOrder"` and `Name = "ShopCreateOrder"`. Two query trains with the same name in different namespaces are allowed, because neither has a response type.

Whatever its GraphQL name, a field runs its train by the interface's FullName, the canonical train name, so two trains whose interfaces share a short name each run their own train.

## Namespaces

Use the `Namespace` property to group related trains under a sub-namespace in the GraphQL schema. This keeps the schema organized as the number of trains grows.

```csharp
[TraxQuery(Namespace = "players", Description = "Looks up a player profile")]
public class LookupPlayerTrain : ServiceTrain<LookupPlayerInput, PlayerProfile>, ILookupPlayerTrain { ... }

[TraxQuery(Namespace = "players", Description = "Searches for players")]
public class SearchPlayersTrain : ServiceTrain<SearchPlayersInput, SearchResult>, ISearchPlayersTrain { ... }

[TraxMutation(Namespace = "alerts", Description = "Creates a new alert")]
public class CreateAlertTrain : ServiceTrain<CreateAlertInput, Alert>, ICreateAlertTrain { ... }
```

This produces the following schema structure:

```graphql
query {
  discover {
    players {
      lookupPlayer(input: { playerId: "player-42" }) { ... }
      searchPlayers(input: { query: "ace" }) { ... }
    }
  }
}

mutation {
  dispatch {
    alerts {
      createAlert(input: { message: "Server overloaded" }) { ... }
    }
  }
}
```

Trains without `Namespace` remain at the root level of `discover` or `dispatch`. Both namespaced and non-namespaced trains can coexist. The `[TraxQueryModel]` attribute also supports `Namespace` for entity queries.

Typed GraphQL clients targeting a namespaced train pair `Namespace` with the client's `Path` property on `[GraphQLOperation]`. For the schema above, a typed client would use `Path = "discover.players"` and `RootField = "lookupPlayer"`. See the [GraphQL Client docs](/docs/api-graphql-client#nested-envelopes-path).

## Examples

A `[TraxQuery]`/`[TraxMutation]` train is exposed via GraphQL, so it must declare its authorization posture explicitly: `[TraxAuthorize]` to gate it or `[TraxAllowAnonymous]` to open it. A train with neither fails at host startup (`AddTraxGraphQL`), unless the endpoint is gated with `RequireAuthorization()`. See [Authorization guide - Required Exposure Posture](/docs/authorization#required-exposure-posture).

### Query train (read-only, synchronous)

```csharp
[TraxQuery(Description = "Looks up a player profile")]
[TraxAllowAnonymous]
public class LookupPlayerTrain
    : ServiceTrain<LookupPlayerInput, LookupPlayerOutput>, ILookupPlayerTrain
{
    protected override Task<Either<Exception, LookupPlayerOutput>> Junctions() => Chain<FetchPlayerJunction>().Resolve();
}
```

Generates the query field `lookupPlayer` under `discover`. Returns `LookupPlayerOutput` directly:

```graphql
query {
  discover {
    lookupPlayer(input: { playerId: "player-42" }) {
      playerId
      rank
      wins
      losses
      rating
    }
  }
}
```

### Default mutation (Run + Queue)

```csharp
[TraxAuthorize("Admin")]
[TraxMutation(Description = "Bans a player (admin only)")]
public class BanPlayerTrain : ServiceTrain<BanPlayerInput, Unit>, IBanPlayerTrain
{
    protected override Task<Either<Exception, Unit>> Junctions() => Chain<ApplyBanJunction>().Resolve();
}
```

With no operations passed, both Run and Queue are enabled. Generates `banPlayer` under `dispatch` with an optional `mode: ExecutionMode` parameter (default `RUN`):

```graphql
# Run synchronously (default)
mutation {
  dispatch {
    banPlayer(input: { playerId: "player-42", reason: "cheating" }) {
      externalId
      metadataId
    }
  }
}

# Queue for async execution
mutation {
  dispatch {
    banPlayer(
      input: { playerId: "player-42", reason: "cheating" }
      mode: QUEUE
      priority: 10
    ) {
      externalId
      workQueueId
    }
  }
}
```

### Run-only mutation (lightweight, synchronous)

```csharp
[TraxMutation(GraphQLOperation.Run, Description = "Pings the server")]
public class PingTrain : ServiceTrain<PingInput, PongOutput>, IPingTrain
{
    protected override Task<Either<Exception, PongOutput>> Junctions() => Chain<PingJunction>().Resolve();
}
```

Generates only `ping` under `dispatch`. No `mode` or `priority` parameters.

### Queue-only mutation (heavy, asynchronous)

```csharp
[TraxMutation(GraphQLOperation.Queue, Description = "Recalculates the leaderboard")]
public class RecalculateLeaderboardTrain
    : ServiceTrain<RecalculateLeaderboardInput, Unit>, IRecalculateLeaderboardTrain
{
    protected override Task<Either<Exception, Unit>> Junctions() =>
        Chain<AggregateScoresJunction>().Chain<RankPlayersJunction>().Resolve();
}
```

Generates only `recalculateLeaderboard` under `dispatch`. Has `priority` but no `mode` parameter.

### Both execution modes (explicit)

```csharp
[TraxMutation(GraphQLOperation.Run, GraphQLOperation.Queue, Description = "Processes a completed match result")]
public class ProcessMatchResultTrain
    : ServiceTrain<ProcessMatchResultInput, ProcessMatchResultOutput>, IProcessMatchResultTrain { ... }
```

This is equivalent to `[TraxMutation]` with no operations. Both forms enable both modes. Generates `processMatchResult` under `dispatch` with `mode: ExecutionMode` parameter.

### Deprecating a field

```csharp
[TraxMutation(DeprecationReason = "Use BanPlayerV2Train instead")]
public class BanPlayerTrain : ServiceTrain<BanPlayerInput, Unit>, IBanPlayerTrain { ... }
```

The generated mutation will show a deprecation warning in GraphQL introspection.

## Discovery

The attribute metadata is available through `ITrainDiscoveryService`. Each `TrainRegistration` includes:

- `IsQuery`: whether the train has `[TraxQuery]`
- `IsMutation`: whether the train has `[TraxMutation]`
- `GraphQLName`: the name override (null if auto-derived)
- `GraphQLNamespace`: the namespace grouping (null if at root level)

The `trains` GraphQL query also exposes these fields.

## Package

```
dotnet add package Trax.Effect
```

The attributes are defined in `Trax.Effect` so train classes can use them without depending on `Trax.Api.GraphQL`.
