> ## Documentation Index
> Fetch the complete documentation index at: https://docs.speckle.systems/llms.txt
> Use this file to discover all available pages before exploring further.

# Handling server capabilities

> Ask the server what it supports before you call — the pattern Speckle's own connectors use for multi-deployment compatibility

Not every Speckle server is the same — workspace support, server version, and user permissions all vary. Speckle connectors **ask the server what it supports before attempting an operation**, rather than catching broad failures after the fact.

**When to read this:** multi-deployment apps, connectors, or any code calling version-gated APIs. **Read next:** [Exceptions](/developers/sdks/dotnet/api-reference/exceptions) for `AggregateException` vs direct throws.

## Prerequisites

* [Authentication](/developers/sdks/dotnet/getting-started/authentication) and a connected `IClient`
* [Exceptions](/developers/sdks/dotnet/api-reference/exceptions) — the hierarchy this pattern relies on
* Familiarity with [ServerResource](/developers/sdks/dotnet/api-reference/resources/server), [ProjectResource](/developers/sdks/dotnet/api-reference/resources/project), and [ModelResource](/developers/sdks/dotnet/api-reference/resources/model)

## Complete Example

This mirrors the pattern Speckle's own connectors use to decide whether a server supports connector-scale model ingestion, adapted from `SendOperation.CheckUseModelIngestionSend` in [speckle-sharp-connectors](https://github.com/specklesystems/speckle-sharp-connectors):

```csharp Example lines icon="https://mintcdn.com/speckle/VtRPWzmN-ULoLfIv/images/developers/sdks/csharp.svg?fit=max&auto=format&n=VtRPWzmN-ULoLfIv&q=85&s=f2970e3b8bd4a2c7899b46211a42f251" theme={null}
using Speckle.Sdk;
using Speckle.Sdk.Api;

async Task<bool> CanUseModelIngestion(IClient client, string projectId, string modelId)
{
    try
    {
        var permissionCheck = await client.Model.CanCreateModelIngestion(projectId, modelId);
        permissionCheck.EnsureAuthorised(); // throws WorkspacePermissionException if authorized == false
        return true;
    }
    catch (AggregateException ex) when (ex.InnerExceptions.OfType<SpeckleGraphQLInvalidQueryException>().Any())
    {
        // The server's GraphQL schema doesn't have this field at all — it predates
        // model ingestion support. This is a capability gap, not a permission denial.
        return false;
    }
    // WorkspacePermissionException (a real "no" from a capable server) is intentionally
    // NOT caught here — let it propagate so the caller can tell the user why they were denied.
}
```

Two outcomes look similar from the outside (both mean "don't use this feature") but mean very different things to a user: "your server is too old" versus "you don't have permission." Catching only the specific schema-level exception, and letting a genuine permission failure surface as `WorkspacePermissionException`, keeps that distinction intact.

## Step-by-Step Explanation

### Server-Level Capability Checks

The simplest form: a boolean method that tells you whether a whole feature area exists on this deployment, with no exceptions involved.

```csharp Example lines icon="https://mintcdn.com/speckle/VtRPWzmN-ULoLfIv/images/developers/sdks/csharp.svg?fit=max&auto=format&n=VtRPWzmN-ULoLfIv&q=85&s=f2970e3b8bd4a2c7899b46211a42f251" theme={null}
if (await client.Server.IsWorkspaceEnabled())
{
    var workspaces = await client.ActiveUser.GetWorkspaces();
    // workspace-scoped flow
}
else
{
    // Personal-project flow — most self-hosted community servers land here
    var projects = await client.ActiveUser.GetProjects();
}
```

Speckle's Grasshopper connector uses exactly this check before even attempting to populate a workspace picker — if the server doesn't have workspaces enabled, it skips the workspace UI entirely rather than calling a workspace query and handling the failure.

### Permission Checks Returned Alongside Resources

Several resources return permission information *with* the data, so you can gate UI and code paths before the user attempts anything:

* `client.Project.GetPermissions(projectId)` → `ProjectPermissionChecks` (`canCreateModel`, `canDelete`, `canLoad`; `canPublish` is obsolete — use `ModelPermissionChecks.canCreateVersion`)
* `client.Model.GetPermissions(projectId, modelId)` → `ModelPermissionChecks` (`canUpdate`, `canDelete`, `canCreateVersion`)
* `client.ActiveUser.GetProjectsWithPermissions()` → each `ProjectWithPermissions.permissions` (`canCreateModel`, `canDelete`, `canLoad` only)
* `Workspace.canCreateProject` on a fetched workspace

Speckle's own connectors use these to disable menu items and show a tooltip explaining why, rather than letting the user click "send" and then showing an error:

```csharp Example lines icon="https://mintcdn.com/speckle/VtRPWzmN-ULoLfIv/images/developers/sdks/csharp.svg?fit=max&auto=format&n=VtRPWzmN-ULoLfIv&q=85&s=f2970e3b8bd4a2c7899b46211a42f251" theme={null}
if (!project.permissions.canLoad.authorized)
{
    projectMenuItem.Enabled = false;
    projectMenuItem.ToolTipText = "You do not have permission to do operation on this project.";
}
```

### Explicit "Can I Do This" Checks

For actions that aren't tied to an existing permission field on a resource, some methods ask the question directly and return a `PermissionCheckResult`:

* `client.Model.CanCreateModelIngestion(projectId, modelId)`
* `client.ActiveUser.CanCreatePersonalProjects()`

`PermissionCheckResult` exposes `authorized`, `code`, `message`, and a convenience method:

```csharp Example lines icon="https://mintcdn.com/speckle/VtRPWzmN-ULoLfIv/images/developers/sdks/csharp.svg?fit=max&auto=format&n=VtRPWzmN-ULoLfIv&q=85&s=f2970e3b8bd4a2c7899b46211a42f251" theme={null}
public void EnsureAuthorised(); // throws WorkspacePermissionException if authorized == false
```

<Note>
  The property is `authorized` (US spelling); the guard method is `EnsureAuthorised()` (UK spelling) — both match the SDK.
</Note>

Call `EnsureAuthorised()` as a guard clause instead of hand-rolling an `if (!result.authorized) throw ...` at every call site.

### Telling "Not Allowed" Apart From "Server Doesn't Support This"

This is the part that's easy to get wrong with a blanket `catch`. A server old enough to predate a permission field (like `canCreateIngestion`, added in server 3.0.11) doesn't return `authorized: false` — its GraphQL schema doesn't have the field at all, so the query fails at the schema-validation level. That surfaces as `AggregateException` wrapping a `SpeckleGraphQLInvalidQueryException`, which is structurally different from `WorkspacePermissionException` (thrown directly by `EnsureAuthorised()` when a capable server says no).

Catch only the narrow exception that represents "this server doesn't know this concept," as shown in the [Complete Example](#complete-example) above. Don't catch `SpeckleException` or `AggregateException` broadly around the whole check — that would also swallow a genuine `WorkspacePermissionException` (if you don't re-throw it) or mask unrelated failures (network errors, auth expiry) as capability gaps.

<Info>
  Some older, internal Speckle tooling also uses raw HTTP endpoint probing (checking for a `404` response) to detect feature availability outside GraphQL. That's an implementation detail of specific legacy code paths, not a documented part of the Speckle.Sdk public surface — prefer the GraphQL capability and permission checks above, which are part of the supported API.
</Info>

## Common Pitfalls

<AccordionGroup>
  <Accordion title="Wrapping a mutation in try/catch (Exception) instead of checking first">
    A blanket catch can't tell a user "your server needs an upgrade" apart from "you don't have permission" apart from "the network dropped." Check server capability and permissions first, and let genuinely unexpected exceptions propagate — see [Exceptions](/developers/sdks/dotnet/api-reference/exceptions).
  </Accordion>

  <Accordion title="Assuming workspaces are always enabled">
    Many self-hosted and community servers run without the workspaces module. Code that only exercises `client.Workspace` and workspace-scoped `client.ActiveUser` methods will throw `SpeckleGraphQLWorkspaceNotEnabledException` (wrapped in `AggregateException`) on those servers. Check `client.Server.IsWorkspaceEnabled()` and support the personal-project path too.
  </Accordion>

  <Accordion title="Catching AggregateException without inspecting InnerExceptions">
    An unqualified `catch (AggregateException)` around a capability check treats old-server incompatibility and a real permission denial identically. Filter with an exception `when` clause (as in the Complete Example) or inspect `InnerExceptions` before deciding how to respond.
  </Accordion>

  <Accordion title="Expecting EnsureAuthorised() to throw an AggregateException">
    `PermissionCheckResult.EnsureAuthorised()` throws `WorkspacePermissionException` directly — it is not wrapped, because it isn't a GraphQL response error. Don't put it inside a `catch (AggregateException)` block expecting to catch it there. See [Exceptions: Workspace Permission Errors](/developers/sdks/dotnet/api-reference/exceptions#workspace-permission-errors).
  </Accordion>
</AccordionGroup>

## Next Steps

<CardGroup cols={3}>
  <Card title="Core concepts" icon="book" href="/developers/sdks/dotnet/concepts/overview">
    Server capability patterns in context
  </Card>

  <Card title="Publish large models (connectors)" icon="upload" href="/developers/sdks/dotnet/guides/building-connector-scale-sends">
    CanCreateModelIngestion in upload flow
  </Card>

  <Card title="ServerResource" icon="code" href="/developers/sdks/dotnet/api-reference/resources/server">
    IsWorkspaceEnabled API surface
  </Card>
</CardGroup>
