Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 109 additions & 0 deletions packages/http-client-csharp/DYNAMIC_MODEL_IMPLEMENTATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# @dynamicModel Decorator Implementation

## Overview

This implementation adds support for the `@dynamicModel` decorator in @typespec/http-client-csharp. When applied to a model, it enables AdditionalProperties-based serialization using the new System.ClientModel AdditionalProperties struct instead of the traditional `_serializedAdditionalRawData` dictionary approach.

## Usage Example

```typespec
import "@typespec/http-client-csharp";
using TypeSpec.CSharp;

@dynamicModel
model User {
id: string;
name: string;
email?: string;
}

op getUser(): User;
```

## Generated C# Code Comparison

### Traditional Approach (without @dynamicModel)

```csharp
public partial class User
{
private readonly IDictionary<string, BinaryData> _serializedAdditionalRawData;

public User(string id, string name, string email = null, IDictionary<string, BinaryData> serializedAdditionalRawData = null)
{
Id = id;
Name = name;
Email = email;
_serializedAdditionalRawData = serializedAdditionalRawData;
}

public string Id { get; }
public string Name { get; }
public string Email { get; }
}
```

### New Approach (with @dynamicModel)

```csharp
public partial class User
{
public User(string id, string name, string email = null, AdditionalProperties patch = default)
{
Id = id;
Name = name;
Email = email;
Patch = patch;
}

public string Id { get; }
public string Name { get; }
public string Email { get; }
public AdditionalProperties Patch { get; set; }
}
```

## Implementation Status

### ✅ Completed
- TypeSpec decorator definition and registration
- Decorator processing pipeline in TypeScript emitter
- Input model type extension to track dynamic model flag
- C# model provider modifications:
- Skip raw data field generation for dynamic models
- Generate Patch property for dynamic models
- Comprehensive test suite

### 🚧 Pending (blocked on System.ClientModel alpha release)
- Update to System.ClientModel 1.6.0-alpha.20250804.4
- Replace object placeholder with actual AdditionalProperties type
- Implement serialization logic modifications:
- Deserialization: Use `AdditionalProperties.Set()` for unknown properties
- Serialization: Check patches and propagate to child objects

## Architecture

The implementation follows a clean pipeline:

1. **TypeSpec Layer**: `@dynamicModel` decorator marks models
2. **Emitter Layer**: Decorator is processed and flag is set on InputModelType
3. **Serialization Layer**: JSON carries the isDynamicModel flag to C# generator
4. **C# Generation Layer**: ModelProvider generates different code based on flag
5. **Generated Code**: Models have either raw data field or Patch property

## Testing

Comprehensive tests cover:
- Basic dynamic model functionality
- Inheritance scenarios
- Models with additional properties
- Multiple dynamic models in same specification
- Negative cases (regular models without decorator)

## Future Work

When System.ClientModel alpha becomes available:
1. Update package reference
2. Replace placeholder type with AdditionalProperties
3. Implement serialization/deserialization logic per the reference implementations
4. Add integration tests with actual serialization scenarios
2 changes: 1 addition & 1 deletion packages/http-client-csharp/emitter/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export { $onEmit } from "./emitter.js";
// we export `createModel` only for autorest.csharp because it uses the emitter to generate the code model file but not calling the dll here
// we could remove this export when in the future we deprecate autorest.csharp
export { createModel } from "./lib/client-model-builder.js";
export { $lib, createDiagnostic, getTracer, reportDiagnostic } from "./lib/lib.js";
export { $lib, $dynamicModel, createDiagnostic, getTracer, reportDiagnostic } from "./lib/lib.js";
export { LoggerLevel } from "./lib/logger-level.js";
export { Logger } from "./lib/logger.js";
export {
Expand Down
19 changes: 18 additions & 1 deletion packages/http-client-csharp/emitter/src/lib/decorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
// Licensed under the MIT License. See License.txt in the project root for license information.

import { SdkContext } from "@azure-tools/typespec-client-generator-core";
import { DecoratedType, Operation, Type } from "@typespec/compiler";
import { DecoratedType, Model, Operation, Type } from "@typespec/compiler";
import { ExternalDocs } from "../type/external-docs.js";
import { $lib } from "./lib.js";

const externalDocsKey = Symbol("externalDocs");
export function getExternalDocs(context: SdkContext, entity: Type): ExternalDocs | undefined {
Expand All @@ -18,6 +19,22 @@ export function getOperationId(context: SdkContext, entity: Operation): string |
return context.program.stateMap(operationIdsKey).get(entity);
}

const dynamicModelKey = Symbol("dynamicModel");
/**
* @returns true if the model is marked with @dynamicModel decorator
*/
export function isDynamicModel(context: SdkContext, entity: Model): boolean {
return context.program.stateMap(dynamicModelKey).get(entity) === true;
}

/**
* Marks a model to use AdditionalProperties-based serialization in C#
* instead of the traditional _serializedAdditionalRawData approach.
*/
export function $dynamicModel(context: SdkContext, target: Model) {
context.program.stateMap(dynamicModelKey).set(target, true);
}

export function hasDecorator(type: DecoratedType, name: string): boolean {
return type.decorators.find((it) => it.decorator.name === name) !== undefined;
}
2 changes: 2 additions & 0 deletions packages/http-client-csharp/emitter/src/lib/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ export const $lib = createTypeSpecLibrary({
},
});

export { $dynamicModel } from "./decorators.js";

/**
* Reports a diagnostic. Defined in the core compiler.
* @beta
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import { describe, it, beforeEach } from "vitest";
import { TestHost } from "@typespec/compiler/testing";
import { strictEqual, ok } from "assert";
import { createModel } from "../../src/lib/client-model-builder.js";
import {
createCSharpSdkContext,
createEmitterContext,
createEmitterTestHost,
typeSpecCompile,
} from "./utils/test-util.js";

describe("Test @dynamicModel decorator functionality", () => {
let runner: TestHost;

beforeEach(async () => {
runner = await createEmitterTestHost();
});

it("should mark simple model as dynamic", async () => {
const program = await typeSpecCompile(
`
import "@typespec/http-client-csharp";
using TypeSpec.CSharp;

@dynamicModel
model SimpleModel {
name: string;
value: int32;
}

op getSimple(): SimpleModel;
`,
runner,
);

const context = createEmitterContext(program);
const sdkContext = await createCSharpSdkContext(context);
const root = createModel(sdkContext);

strictEqual(root.models.length, 1);
const model = root.models[0];
strictEqual(model.name, "SimpleModel");
strictEqual(model.isDynamicModel, true);
});

it("should not mark regular model as dynamic", async () => {
const program = await typeSpecCompile(
`
model RegularModel {
name: string;
value: int32;
}

op getRegular(): RegularModel;
`,
runner,
);

const context = createEmitterContext(program);
const sdkContext = await createCSharpSdkContext(context);
const root = createModel(sdkContext);

strictEqual(root.models.length, 1);
const model = root.models[0];
strictEqual(model.name, "RegularModel");
strictEqual(model.isDynamicModel, false);
});

it("should handle dynamic models with additional properties", async () => {
const program = await typeSpecCompile(
`
import "@typespec/http-client-csharp";
using TypeSpec.CSharp;

@dynamicModel
model ModelWithAdditionalProps {
name: string;
...Record<unknown>;
}

op getWithAdditional(): ModelWithAdditionalProps;
`,
runner,
);

const context = createEmitterContext(program);
const sdkContext = await createCSharpSdkContext(context);
const root = createModel(sdkContext);

strictEqual(root.models.length, 1);
const model = root.models[0];
strictEqual(model.name, "ModelWithAdditionalProps");
strictEqual(model.isDynamicModel, true);
ok(model.additionalProperties, "Model should have additional properties");
});

it("should handle inheritance with dynamic models", async () => {
const program = await typeSpecCompile(
`
import "@typespec/http-client-csharp";
using TypeSpec.CSharp;

model BaseModel {
id: string;
}

@dynamicModel
model DerivedModel extends BaseModel {
name: string;
}

op getDerived(): DerivedModel;
`,
runner,
);

const context = createEmitterContext(program);
const sdkContext = await createCSharpSdkContext(context);
const root = createModel(sdkContext);

// Should have both base and derived models
ok(root.models.length >= 2);

const derivedModel = root.models.find(m => m.name === "DerivedModel");
ok(derivedModel, "Should find derived model");
strictEqual(derivedModel.isDynamicModel, true);

const baseModel = root.models.find(m => m.name === "BaseModel");
ok(baseModel, "Should find base model");
strictEqual(baseModel.isDynamicModel, false);
});

it("should work with multiple dynamic models", async () => {
const program = await typeSpecCompile(
`
import "@typespec/http-client-csharp";
using TypeSpec.CSharp;

@dynamicModel
model FirstDynamic {
first: string;
}

@dynamicModel
model SecondDynamic {
second: int32;
}

model RegularModel {
regular: boolean;
}

op getFirst(): FirstDynamic;
op getSecond(): SecondDynamic;
op getRegular(): RegularModel;
`,
runner,
);

const context = createEmitterContext(program);
const sdkContext = await createCSharpSdkContext(context);
const root = createModel(sdkContext);

strictEqual(root.models.length, 3);

const firstDynamic = root.models.find(m => m.name === "FirstDynamic");
ok(firstDynamic);
strictEqual(firstDynamic.isDynamicModel, true);

const secondDynamic = root.models.find(m => m.name === "SecondDynamic");
ok(secondDynamic);
strictEqual(secondDynamic.isDynamicModel, true);

const regular = root.models.find(m => m.name === "RegularModel");
ok(regular);
strictEqual(regular.isDynamicModel, false);
});
});
Loading
Loading