azure-sdk-for-cpp/eng/common/knowledge/customizing-client-tsp.md
Azure SDK Bot d5b73b7f6b
Sync eng/common directory with azure-sdk-tools for PR 12758 (#6822)
* add reference doc for adding client customizations in TypeSpec

* Update eng/common/knowledge/customizing-client-tsp.md

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: Mariana Rios Flores <mariari@microsoft.com>

---------

Co-authored-by: Christopher Radek <Christopher.Radek@microsoft.com>
Co-authored-by: Christopher Radek <14189820+chrisradek@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Mariana Rios Flores <mariari@microsoft.com>
2025-11-10 10:49:51 -08:00

476 lines
12 KiB
Markdown

# TypeSpec Client Customizations Reference
## Quick Setup
### 1. Project Structure
```
project/
├── main.tsp # Your service definition
├── client.tsp # Client customizations
└── tspconfig.yaml # Compiler configuration
```
### 2. Basic client.tsp Template
```typespec
import "./main.tsp";
import "@azure-tools/typespec-client-generator-core";
using Azure.ClientGenerator.Core;
// Your customizations here
```
## Client Customizations Namespace
`client.tsp` should have a file-level namespace if types (e.g. models, interfaces, operations, etc) are defined in `client.tsp`.
Do not add a file-level namespace if one already exists. They are only required when types are defined in `client.tsp`.
```typespec
import "./main.tsp";
import "@azure-tools/typespec-client-generator-core";
using Azure.ClientGenerator.Core;
namespace ClientCustomizations;
// Your customizations here
```
## Universal Scope Parameter
**IMPORTANT**: All Azure.ClientGenerator.Core decorators support an optional `scope` parameter as their final parameter to target specific language emitters.
### Scope Syntax
```typespec
@decoratorName(/* decorator-specific params */, scope?: string)
```
### Scope Patterns
#### Target Specific Languages
```typespec
// Single language
@@clientName(Foo, "Bar", "python")
@@access(Foo.get, Access.internal, "csharp")
// Multiple languages (comma-separated)
@@clientName(Foo, "Bar", "python, javascript")
```
#### Exclude Languages (Negation)
```typespec
// Exclude one language
@@clientName(Foo, "Bar", "!csharp") // All languages EXCEPT C#
// Exclude multiple languages
@@clientName(Foo, "Bar", "!python, !go") // All languages EXCEPT python and go
```
#### Language Identifiers
- `"csharp"` - C#/.NET
- `"python"` - Python
- `"java"` - Java
- `"javascript"` - TypeScript/JavaScript
- `"go"` - Go
- `"rust"` - Rust
### Scope Best Practices
#### DO: Use negation for single language exclusions
```typespec
// Good: Exclude only C#
@@clientName(get, "getFoo", "!csharp")
// Bad: List all other languages
@@clientName(get, "getFoo", "python, java, javascript, go")
```
#### DO: Combine scopes when logic is identical
```typespec
// Good: Same customization for multiple languages
@@clientName(name, "sharedName", "python, go")
// Avoid: Duplicate decorators
@@clientName(name, "sharedName", "python")
@@clientName(name, "sharedName", "go")
```
#### DON'T: Overuse scopes without clear need
```typespec
// Bad: Unnecessary scope for universal customization
@@clientName(MyService, "MyClient", "csharp, python, java, javascript, go")
// Good: No scope means all languages
@@clientName(MyService, "MyClient")
```
## Core Decorators
### @access
**Purpose**: Control visibility of types and operations in generated clients.
**Syntax**: `@access(value: Access.public | Access.internal, scope?: string)`
**Usage**:
```typespec
// Hide internal operations
@@access(getFoo, Access.internal)
// Make models referenced by internal operations public
@@access(Foo, Access.public)
// Language-specific access
@@access(getFoo, Access.internal, "csharp")
```
**Propagation Rules**:
- Operations marked `Access.internal` make their models internal
- Operations marked `Access.public` make their models public
- Namespace access propagates to contained types
- Model access propagates to properties and inheritance hierarchy
### @client
**Purpose**: Define root clients in the SDK.
**Restrictions**: Cannot be used with `@clientLocation` decorator. Cannot be used as an augmentation (`@@`) decorator.
**Important**: `@client` has to be used on a type defined in `client.tsp`, so a file-level namespace (e.g. `namespace ClientCustomizations;`) should be added if one does not exist.
**Syntax**: `@client(options: ClientOptions, scope?: string)`
**Usage**:
```typespec
// Basic client
@client({ service: MyService })
interface MyClient {}
// Named client
@client({ service: MyService, name: "CustomClient" })
interface MyClient {}
// Split operations into multiple clients
@client({ service: PetStore, name: "FoodClient" })
interface FoodClient {
feed is PetStore.feed;
}
@client({ service: PetStore, name: "PetClient" })
interface PetClient {
pet is PetStore.pet;
}
```
### @operationGroup
**Purpose**: Define sub-clients (operation groups).
**Restrictions**: Cannot be used with `@clientLocation` decorator. Cannot be used as an augmentation (`@@`) decorator.
**Important**: `@operationGroup` has to be used on a type defined in `client.tsp`, so a file-level namespace (e.g. `namespace ClientCustomizations;`) should be added if one does not exist.
**Syntax**: `@operationGroup(scope?: string)`
**Usage**:
```typespec
@client({ service: MyService })
namespace MyClient;
@operationGroup
interface Pets {
list is MyService.listPets;
get is MyService.getPet;
}
@operationGroup
interface Users {
list is MyService.listUsers;
create is MyService.createUser;
}
```
### @clientLocation
**Purpose**: Move operations between clients without restructuring.
**Restrictions**: Cannot be used with `@client` or `@operationGroup` decorators.
**Syntax**: `@clientLocation(target: Interface | Namespace | string, scope?: string)`
**Usage**:
```typespec
// Move to existing client
@@clientLocation(MyService.upload, AdminOperations);
// Move to new client
@@clientLocation(MyService.archive, "ArchiveClient");
// Move to root client
@@clientLocation(MyService.SubClient.health, MyService);
// Move parameter to client initialization
@@clientLocation(MyService.upload.subscriptionId, MyService);
```
### @clientName
**Purpose**: Override generated names for SDK elements. Takes precedence over all other naming mechanisms.
**Important**: Always use PascalCase or camelCase for the rename parameter to make it easier to combine language scopes. SDKs will apply language-specific naming conventions automatically.
**Syntax**: `@clientName(rename: string, scope?: string)`
**Usage**:
```typespec
// Rename Type
@@clientName(PetStore, "PetStoreClient");
// Language-specific names
@@clientName(foo, "pythonicFoo", "python")
@@clientName(foo, "CSharpFoo", "csharp")
```
### @clientNamespace
**Purpose**: Change the namespace/package of generated types in the client SDK.
**Syntax**: `@clientNamespace(rename: string, scope?: string)`
**Usage**:
```typespec
// Change client namespace
@@clientNamespace(MyService, "MyClient");
// Move model to different namespace
@@clientNamespace(MyService.MyModel, "MyClient.Models")
```
### @clientInitialization
**Purpose**: Add custom parameters to client initialization.
**Important**: When `@clientInitialization` references a model defined in `client.tsp`, add a file-level namespace (e.g. `namespace ClientCustomizations;`) if one does not exist.
**Syntax**: `@clientInitialization(options: ClientInitializationOptions, scope?: string)`
**Usage**:
```typespec
// Add initialization parameters
model MyClientOptions {
connectionString: string;
}
@@clientInitialization(MyService, { parameters: MyClientOptions });
// With parameter aliasing
model MyClientOptions {
@paramAlias("subscriptionId")
subId: string;
}
@@clientInitialization(MyService, { parameters: MyClientOptions });
```
### @alternateType
**Purpose**: Replace types in generated clients.
**Syntax**: `@alternateType(alternate: Type | ExternalType, scope?: string)`
**Usage**:
```typespec
// Change property type
@@alternateType(Foo.date, string);
// Language-specific alternates
@@alternateType(Foo.date, string, "python")
// External type replacement
@@alternateType(uri, {
identity: "System.Uri",
package: "System",
}, "csharp")
```
### @override
**Purpose**: Customize method signatures in generated clients.
**Restrictions**: Only operation parameter signatures can be customized.
**Important**: When `@override` references an operation defined in `client.tsp`, a file-level namespace (e.g. `namespace ClientCustomizations;`) should be added if one does not exist.
**Syntax**: `@override(override: Operation, scope?: string)`
**Usage**:
```typespec
// main.tsp
// Original operation
op myOperation(foo: string, bar: string): void;
// client.tsp
// Custom signature - combine into options
model MyOperationOptions {
foo: string;
bar: string;
}
op myOperationCustom(options: MyOperationOptions): void;
@@override(myOperation, myOperationCustom);
```
### @scope
**Purpose**: Include/exclude operations from specific languages.
**Usage**:
```typespec
@@scope(Foo.create, "!csharp") // All languages except C#
@@scope(Foo.create, "python") // Python only
@@scope(Foo.create, "java, go") // Java and Go only
```
### @usage
**Purpose**: Add usage information to models and enums.
**Note**: The usages provided are _additive_.
**Usage**:
```typespec
// Add input and output usage to type
@@usage(MyModel, Usage.input | Usage.output)
```
**Usage Values**:
- `Usage.input`: Used in request
- `Usage.output`: Used in response
- `Usage.json`: Used with JSON content type
- `Usage.xml`: Used with XML content type
### @clientDoc
**Purpose**: Override documentation for client libraries.
**Usage**:
```typespec
// Replace type documentation with client documentation
@@clientDoc(myOperation, "Client-specific documentation", DocumentationMode.replace)
// Append type documentation with client documentation - for only python
@@clientDoc(myModel, "Additional client notes", DocumentationMode.append, "python")
```
## Language-specific Customizations
### @useSystemTextJsonConverter (C# only)
**Purpose**: Use custom JSON converter for backward compatibility.
**Usage**:
```typespec
@@useSystemTextJsonConverter(MyModel, "csharp")
```
## Best Practices
### Do's
- Use `client.tsp` for all customizations
- Use scope parameter for language-specific customizations
- Prefer scope negation (`"!csharp"`) when excluding single languages
- Combine scopes (`"python, java"`) when logic is identical across languages
- Use a file-level namespace (e.g. `namespace ClientCustomizations;`) in `client.tsp` if any types are defined in `client.tsp`.
### Don'ts
- Mix `@clientLocation` with `@client`/`@operationGroup`
- Over-customize - prefer defaults when possible
- Use legacy decorators for new services
- Rename without considering the impact on breaking changes
- Forget to specify scope for language-specific customizations
## Common Scenarios
### Scenario 1: Move Operations to Root Client
```typespec
// Before: Operations in interfaces become sub-clients
interface Pets { feed(): void; }
interface Users { login(): void; }
// After: All operations on root client
@@clientLocation(Pets.feed, MyService);
@@clientLocation(Users.login, MyService);
```
### Scenario 2: Rename for Consistency
```typespec
// Standardize naming across languages
@@clientName(MyService, "MyServiceClient");
@@clientName(getUserInfo, "GetUserInformation");
// Language-specific naming
@@clientName(uploadFile, "UploadFile", "csharp, python");
```
### Scenario 3: Add Client Parameters
```typespec
// Elevate common parameters to client
model MyServiceOptions {
subscriptionId: string;
resourceGroup: string;
}
@@clientInitialization(MyService, { parameters: MyServiceOptions });
```
### Scenario 4: Multi-Client Architecture
```typespec
// Separate admin and user operations
@client({ service: MyService, name: "AdminClient" })
interface AdminClient {
deleteUser is MyService.deleteUser;
manageRoles is MyService.manageRoles;
}
@client({ service: MyService, name: "UserClient" })
interface UserClient {
getProfile is MyService.getProfile;
updateProfile is MyService.updateProfile;
}
```
### Scenario 5: Language-specific clients
```typespec
// Different client names for Java and others
@client({ service: MyService, name: "Foo.MyServiceClient" }, "java")
@client({ service: MyService, name: "MyServiceClient" }, "!java")
interface MyServiceClient {
getAllData is MyService.getAllData;
}
// Different clients for python and Go
@client({ service: MyService, name: "MyClient" }, "python")
interface MyClientPython {
fetchData is MyService.fetchData;
}
@client({ service: MyService, name: "MyClient" }, "go")
interface MyClientGo {
fetchStream is MyService.fetchStream;
}
```
### Scenario 6: Rename custom client operations
```typespec
// Create a client with operation names changed
@client({ service: MyService, name: "MyClient" })
interface MyClient {
getFoo is MyService.getFooData;
}
```
This reference provides the essential patterns and decorators for TypeSpec client customizations. Focus on the core decorators (`@client`, `@operationGroup`, `@@clientLocation`, `@@clientName`, `@@access`) for most scenarios, and use advanced features selectively.