Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Modify how arrays are resolved by default #6428

Open
wants to merge 29 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
9f6a048
Change how arrays types are treated and resolved
AlitzelMendez Mar 12, 2025
bcfd073
Add test for
AlitzelMendez Mar 13, 2025
cdd992c
Merge branch 'main' into change-default-arrays-cs
AlitzelMendez Mar 13, 2025
20fcdf4
Fix constraing array validator to handle ISet and IEnumerabl
AlitzelMendez Mar 17, 2025
4b2e45a
Merge branch 'main' into change-default-arrays-cs
AlitzelMendez Mar 17, 2025
e127f48
fix merge conflict
AlitzelMendez Mar 17, 2025
8dfbaf7
Merge branch 'main' into change-default-arrays-cs
AlitzelMendez Mar 21, 2025
26be5ee
Feedback:
AlitzelMendez Mar 22, 2025
911def5
Merge branch 'change-default-arrays-cs' of https://github.com/Alitzel…
AlitzelMendez Mar 22, 2025
ce1b82c
Update tests added when merging main into branch
AlitzelMendez Mar 24, 2025
ccac544
Fix missing elements on Mocks
AlitzelMendez Mar 24, 2025
7a7c618
consider ISet scenario
AlitzelMendez Mar 24, 2025
d062d84
Revert ICollection until I clarify spectations on this
AlitzelMendez Mar 24, 2025
91c8f71
Merge branch 'main' into change-default-arrays-cs
AlitzelMendez Mar 24, 2025
87223ad
Feedback
AlitzelMendez Mar 28, 2025
077ab86
Merge branch 'main' into change-default-arrays-cs
AlitzelMendez Mar 28, 2025
1be3579
summary of changes
AlitzelMendez Mar 28, 2025
89c0bd1
typo
AlitzelMendez Mar 28, 2025
d800de0
Merge branch 'main' into change-default-arrays-cs
AlitzelMendez Mar 28, 2025
18662be
modify summary of changes
AlitzelMendez Mar 28, 2025
e8317df
Merge branch 'main' into change-default-arrays-cs
AlitzelMendez Mar 31, 2025
ddd6f4b
correctly resolve pnpm
AlitzelMendez Mar 31, 2025
aad2f05
Merge branch 'main' into change-default-arrays-cs
AlitzelMendez Mar 31, 2025
34866b7
correctly resolve pnpm
AlitzelMendez Mar 31, 2025
478bc54
Update .chronus/changes/change-default-arrays-cs-2025-2-28-13-55-13.md
AlitzelMendez Mar 31, 2025
98bbda0
Merge branch 'main' into change-default-arrays-cs
AlitzelMendez Mar 31, 2025
8ed3e80
optional collection-type
AlitzelMendez Apr 1, 2025
466f77e
docs & summary of changes
AlitzelMendez Apr 1, 2025
11579f1
test fixes
AlitzelMendez Apr 1, 2025
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
19 changes: 19 additions & 0 deletions .chronus/changes/change-default-arrays-cs-2025-2-28-13-55-13.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
changeKind: breaking
packages:
- "@typespec/http-server-csharp"
---
### Change in Array Scaffolding from TypeSpec to C#

The default behavior for scaffolding arrays remains unchanged: arrays will continue to be scaffolded as `T[]` by default. However, for arrays decorated with the `@uniqueItems` decorator, they will now be scaffolded as `ISet<T>`, with `HashSet<T>` as the default implementation.

Additionally, a new emitter option, `collection-type`, has been introduced to provide flexibility in how collections are generated:
- **`collection-type`**:
- **`array` (default)**: Generates arrays (`T[]`).
- **`enumerable`**: Generates `IEnumerable<T>` for collections, with `List<T>` used as the default implementation when needed.

#### Unique Items
For arrays decorated with the `@uniqueItems` decorator, they will be scaffolded as `ISet<T>`, regardless of the `collection-type` option, with `HashSet<T>` as the default implementation.

#### Byte Arrays
The `bytes` type will always be treated as an array of bytes (`byte[]`) in C#, regardless of the `collection-type` option selected.
6 changes: 6 additions & 0 deletions packages/http-server-csharp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,9 @@ The service http port when hosting the project locally.
**Type:** `number`

The service https port when hosting the project locally.

### `collection-type`

**Type:** `"array" | "enumerable"`

Specifies the collection type to use: 'array' or 'enumerable'. The default is 'array'.
4 changes: 3 additions & 1 deletion packages/http-server-csharp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@
"@typespec/compiler": "workspace:^",
"@typespec/http": "workspace:^",
"@typespec/rest": "workspace:^",
"@typespec/versioning": "workspace:^"
"@typespec/versioning": "workspace:^",
"@typespec/json-schema": "workspace:^"
},
"dependencies": {
"@typespec/asset-emitter": "workspace:^",
Expand All @@ -83,6 +84,7 @@
"@typespec/spector": "workspace:^",
"@typespec/tspd": "workspace:^",
"@typespec/versioning": "workspace:^",
"@typespec/json-schema": "workspace:^",
"@vitest/coverage-v8": "^3.0.9",
"@vitest/ui": "^3.0.9",
"fs-extra": "^11.2.0",
Expand Down
14 changes: 14 additions & 0 deletions packages/http-server-csharp/src/cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ async function main() {
description: "Path to the directory where the project will be created.",
type: "string",
})
.option("collection-type", {
description:
"Specifies the type of collection to use: 'array' or 'enumerable'. If not specified, 'array' will be used by default.",
type: "string",
default: "array",
choices: ["array", "enumerable"],
})
.positional("path-to-spec", {
description: "The path to the TypeSpec spec or TypeSpec project directory",
type: "string",
Expand All @@ -64,6 +71,7 @@ async function main() {
const useSwagger: boolean = args["use-swaggerui"];
const overwrite: boolean = args["overwrite"];
const projectName: string = args["project-name"];
const collectionType: string = args["collectionType"] ?? "array";
const httpPort: number = args["http-port"] || (await getFreePort(5000, 5999));
const httpsPort: number = args["https-port"] || (await getFreePort(7000, 7999));
console.log(
Expand Down Expand Up @@ -92,6 +100,12 @@ async function main() {
if (httpsPort) {
compileArgs.push("--option", `@typespec/http-server-csharp.https-port=${httpsPort}`);
}
if (collectionType) {
compileArgs.push(
"--option",
`@typespec/http-server-csharp.collection-type=${collectionType}`,
);
}

const swaggerArgs: string[] = [
"--emit",
Expand Down
112 changes: 86 additions & 26 deletions packages/http-server-csharp/src/lib/boilerplate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -330,44 +330,52 @@ function getArrayConstraintConverter(): string {

public override JsonConverter? CreateConverter(Type typeToConvert)
{
return new ConstrainedArrayConverter<T>(_minItems, _maxItems);
if (typeof(ISet<T>).IsAssignableFrom(typeToConvert))
{
return new ConstrainedSetConverter<T>(_minItems, _maxItems);
}
else if (typeToConvert.IsArray && typeToConvert.GetElementType() == typeof(T))
{
return new ConstrainedStandardArrayConverter<T>(_minItems, _maxItems);
}
else
{
return new ConstrainedEnumerableConverter<T>(_minItems, _maxItems);
}
}


}

public class ConstrainedArrayConverter<T> : JsonConverter<T[]>
public abstract class ConstrainedCollectionConverter<T, TCollection> : JsonConverter<TCollection>
{
public ConstrainedArrayConverter(int? min, int? max) : base()
protected ConstrainedCollectionConverter(int? min, int? max)
{
_minItems = min;
_maxItems = max;
}

internal int? _minItems, _maxItems;
protected int? _minItems, _maxItems;
public JsonConverter<T>? InnerConverter { get; set; }

public virtual Func<ConstrainedArrayConverter<T>, JsonSerializerOptions, JsonConverter<T>> InnerConverterFactory { get; set; } = ConverterHelpers.GetStandardInnerConverter<T>;
public virtual Func<ConstrainedCollectionConverter<T, TCollection>, JsonSerializerOptions, JsonConverter<T>> InnerConverterFactory { get; set; } = ConverterHelpers.GetStandardInnerConverter<T, TCollection>;


internal bool ValidateMin(int count)
protected bool ValidateMin(int count)
{
return !_minItems.HasValue || count >= _minItems.Value;
}

internal bool ValidateMax(int count)
protected bool ValidateMax(int count)
{
return !_maxItems.HasValue || count <= _maxItems.Value;
}

internal void ValidateRange(int count)
protected void ValidateRange(int count)
{
if (!ValidateMax(count) || !ValidateMin(count))
{
throw new JsonException($"Number of array elements not in range [{(_minItems.HasValue ? _minItems.Value : 0)}, {(_maxItems.HasValue ? _maxItems.Value : Array.MaxLength)}]");
}
}
public override T[]? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
public override TCollection? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var _innerConverter = InnerConverterFactory(this, options);
if (reader.TokenType != JsonTokenType.StartArray) { throw new JsonException("Expected start of array"); }
Expand All @@ -382,30 +390,52 @@ function getArrayConstraintConverter(): string {
count++;
}

return list.ToArray();

return ConvertToCollection(list);

}

public override void Write(Utf8JsonWriter writer, T[] value, JsonSerializerOptions options)
public override void Write(Utf8JsonWriter writer, TCollection value, JsonSerializerOptions options)
{
var _innerConverter = InnerConverterFactory(this, options);
writer.WriteStartArray();
for (int i = 0; i < value.Length; ++i)
_innerConverter.Write(writer, value[i], options);
foreach (var item in GetEnumerable(value))
_innerConverter.Write(writer, item, options);
writer.WriteEndArray();
}

protected abstract TCollection ConvertToCollection(List<T> list);
protected abstract IEnumerable<T> GetEnumerable(TCollection collection);
}

public class ConstrainedEnumerableConverter<T> : ConstrainedCollectionConverter<T, IEnumerable<T>>
{
public ConstrainedEnumerableConverter(int? min, int? max) : base(min, max) { }
protected override IEnumerable<T> ConvertToCollection(List<T> list) => list;
protected override IEnumerable<T> GetEnumerable(IEnumerable<T> collection) => collection;
}

public class ConstrainedSetConverter<T> : ConstrainedCollectionConverter<T, ISet<T>>
{
public ConstrainedSetConverter(int? min, int? max) : base(min, max) { }
protected override ISet<T> ConvertToCollection(List<T> list) => new HashSet<T>(list);
protected override IEnumerable<T> GetEnumerable(ISet<T> collection) => collection;
}

public class ConstrainedStandardArrayConverter<T> : ConstrainedCollectionConverter<T, T[]>
{
public ConstrainedStandardArrayConverter(int? min, int? max) : base(min, max) { }
protected override T[] ConvertToCollection(List<T> list) => list.ToArray();
protected override IEnumerable<T> GetEnumerable(T[] collection) => collection;
}

internal static class ConverterHelpers
{
internal static JsonConverter<T> GetStandardInnerConverter<T>(this ConstrainedArrayConverter<T> converter, JsonSerializerOptions options)
internal static JsonConverter<T> GetStandardInnerConverter<T, TCollection>(this ConstrainedCollectionConverter<T, TCollection> converter, JsonSerializerOptions options)
{
if (converter.InnerConverter == null)
{
converter.InnerConverter = (JsonConverter<T>)options.GetConverter(typeof(T));
}

return converter.InnerConverter;
}
}
Expand Down Expand Up @@ -439,9 +469,23 @@ public class NumericArrayConstraintAttribute<T> : ArrayConstraintAttribute<T> wh

public override JsonConverter? CreateConverter(Type typeToConvert)
{
var result = base.CreateConverter(typeToConvert) as ConstrainedArrayConverter<T>;
if (result != null) result.InnerConverterFactory = (c, o) => new NumericJsonConverter<T>(MinValue, MaxValue, MinValueExclusive, MaxValueExclusive, o);
return result;
var result = base.CreateConverter(typeToConvert);
var resultSet = result as ConstrainedSetConverter<T>;
if (resultSet != null) {
resultSet.InnerConverterFactory = (c, o) => new NumericJsonConverter<T>(MinValue, MaxValue, MinValueExclusive, MaxValueExclusive, o);
return resultSet;
}
var resultEnumerable = result as ConstrainedEnumerableConverter<T>;
if (resultEnumerable != null) {
resultEnumerable.InnerConverterFactory = (c, o) => new NumericJsonConverter<T>(MinValue, MaxValue, MinValueExclusive, MaxValueExclusive, o);
return resultEnumerable;
}
var resultStandardArray = result as ConstrainedStandardArrayConverter<T>;
if (resultStandardArray != null) {
resultStandardArray.InnerConverterFactory = (c, o) => new NumericJsonConverter<T>(MinValue, MaxValue, MinValueExclusive, MaxValueExclusive, o);
return resultStandardArray;
}
throw new InvalidOperationException($"Cannot create converter for {typeToConvert} with {this}");
}
}
}`;
Expand Down Expand Up @@ -469,11 +513,27 @@ public class StringArrayConstraintAttribute : ArrayConstraintAttribute<string>
public int MaxItemLength { get { return _maxItemLength.HasValue ? _maxItemLength.Value : 0; } set { _maxItemLength = value; } }
public string? Pattern { get; set; }

override public JsonConverter<string[]> CreateConverter(Type typeToConvert)
override public JsonConverter? CreateConverter(Type typeToConvert)
{
var result = base.CreateConverter(typeToConvert) as ConstrainedArrayConverter<string>;
result!.InnerConverterFactory = (c, o) => new StringJsonConverter(MinItemLength, MaxItemLength, Pattern, o);
return result;
var result = base.CreateConverter(typeToConvert);
var resultSet = result as ConstrainedSetConverter<string>;
if (resultSet != null) {
resultSet.InnerConverterFactory = (c, o) => new StringJsonConverter(MinItemLength, MaxItemLength, Pattern, o);
return resultSet;
}

var resultEnumerable = result as ConstrainedEnumerableConverter<string>;
if (resultEnumerable != null) {
resultEnumerable.InnerConverterFactory = (c, o) => new StringJsonConverter(MinItemLength, MaxItemLength, Pattern, o);
return resultEnumerable;
}

var resultStandardArray = result as ConstrainedStandardArrayConverter<string>;
if (resultStandardArray != null) {
resultStandardArray.InnerConverterFactory = (c, o) => new StringJsonConverter(MinItemLength, MaxItemLength, Pattern, o);
return resultStandardArray;
}
throw new InvalidOperationException($"Cannot create converter for {typeToConvert} with {this}");
}
}
}`;
Expand Down
71 changes: 70 additions & 1 deletion packages/http-server-csharp/src/lib/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,14 +77,83 @@ export class CSharpType implements CSharpTypeMetadata {
}
}
public getTypeReference(scope?: Scope<string>): string {
return `${this.isNamespaceInScope(scope) ? "" : this.namespace + "."}${this.name}`;
if (this.isNamespaceInScope(scope)) {
return this.name;
}

return `${this.namespace}.${this.name}`;
}

public equals(other: CSharpType | undefined): boolean {
return this.name === other?.name && this.namespace === other?.namespace;
}
}

export enum CollectionType {
ISet = "ISet",
ICollection = "ICollection",
IEnumerable = "IEnumerable",
Array = "[]",
}

export function resolveCollectionType(option?: string): CollectionType {
switch (option) {
case "enumerable":
return CollectionType.IEnumerable;
case "array":
default:
return CollectionType.Array;
}
}

export class CSharpCollectionType extends CSharpType {
collectionType: CollectionType;
itemTypeName: string;

static readonly implementationType: Record<CollectionType, string> = {
[CollectionType.ISet]: "HashSet",
[CollectionType.ICollection]: "List",
[CollectionType.IEnumerable]: "List",
[CollectionType.Array]: "[]",
};

public constructor(
csharpType: {
name: string;
namespace: string;
isBuiltIn?: boolean;
isValueType?: boolean;
isNullable?: boolean;
isClass?: boolean;
isCollection?: boolean;
},
collectionType: CollectionType,
itemTypeName: string,
) {
super(csharpType);
this.collectionType = collectionType;
this.itemTypeName = itemTypeName;
}

public getTypeReference(scope?: Scope<string> | undefined): string {
if (this.isNamespaceInScope(scope)) {
return this.name;
}
return `${this.collectionType}<${this.namespace}.${this.itemTypeName}>`;
}

public getImplementationType(): string {
switch (this.collectionType) {
case CollectionType.ISet:
case CollectionType.ICollection:
case CollectionType.IEnumerable:
return `new ${CSharpCollectionType.implementationType[this.collectionType]}<${this.itemTypeName}>()`;
default:
return `[]`;
}
}
}

export abstract class CSharpValue {
value?: any;
public abstract emitValue(scope?: Scope<string>): string;
Expand Down
Loading
Loading