Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
483df56
Initial plan
Copilot Jul 15, 2025
bd6ae2f
Update System.ClientModel to 1.5.1 and add ModelReaderWriterBuildable…
Copilot Jul 15, 2025
92abab4
Complete implementation of ModelReaderWriterBuildableAttribute genera…
Copilot Jul 15, 2025
b8ae844
Revert global.json to original .NET SDK version 9.0.102
Copilot Jul 15, 2025
d972d7d
Resolve merge conflict in Packages.Data.props - keep System.ClientMod…
Copilot Jul 15, 2025
8cb6c8a
Resolve merge conflicts and fix SDK version compatibility
Copilot Jul 15, 2025
c748252
Revert global.json changes and add comprehensive ModelReaderWriterCon…
Copilot Jul 15, 2025
b18e0b0
Fix ModelReaderWriterBuildableAttribute generation to ensure proper u…
Copilot Jul 15, 2025
7c1c995
Update ModelReaderWriterContextDefinition tests with exact count asse…
Copilot Jul 15, 2025
b96b301
Fix compilation error by replacing non-existent IsGenericTypeDefiniti…
Copilot Jul 15, 2025
b5dc1dd
Merge branch 'main' of https://github.com/microsoft/typespec into cop…
JoshLove-msft Jul 16, 2025
ec49c32
Fix duplicate handling
JoshLove-msft Jul 16, 2025
2da1546
fix script and visitor
JoshLove-msft Jul 16, 2025
b784c2a
remove invalid attributes
JoshLove-msft Jul 16, 2025
f59e15a
working
JoshLove-msft Jul 17, 2025
c36c26c
fix
JoshLove-msft Jul 17, 2025
861e842
force deps
JoshLove-msft Jul 17, 2025
3ec0fed
Merge latest changes from main
Copilot Jul 17, 2025
8f5dd17
Fix TypeOfExpression to ensure proper using statements are generated
Copilot Jul 17, 2025
e87c0ba
Revert TypeOfExpression.cs changes as requested
Copilot Jul 17, 2025
5e78717
regen
JoshLove-msft Jul 18, 2025
0ef418f
update perf for algorithm
m-nash Jul 18, 2025
b5878b6
update perf for algorithm (#7962)
m-nash Jul 18, 2025
fd1eb65
regen
m-nash Jul 18, 2025
5d6afea
Merge branch 'copilot/fix-7893' of https://github.com/Microsoft/types…
m-nash Jul 18, 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
2 changes: 1 addition & 1 deletion packages/http-client-csharp/eng/scripts/Generate.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ if (-not $LaunchOnly) {

Write-Host "Installing SampleTypeSpec plugins" -ForegroundColor Cyan

Invoke "npm install" $sampleDir
Invoke "npm install --force" $sampleDir

Write-Host "Generating SampleTypeSpec using plugins" -ForegroundColor Cyan

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("Microsoft.TypeSpec.Generator.ClientModel.Tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010041df4fe80c5af6ff9a410db5a173b0ce24ad68764c623e308b1584a88b1d1d82277f746c1cccba48997e13db3366d5ed676576ffd293293baf42c643f008ba2e8a556e25e529c0407a38506555340749559f5100e6fd78cc935bb6c82d2af303beb0d3c6563400659610759b4ed5cb2e0faf36b17e6842f04cdc544c74e051ba")]
[assembly: InternalsVisibleTo("Microsoft.TypeSpec.Generator.Tests.Perf, PublicKey=002400000480000094000000060200000024000052534131000400000100010041df4fe80c5af6ff9a410db5a173b0ce24ad68764c623e308b1584a88b1d1d82277f746c1cccba48997e13db3366d5ed676576ffd293293baf42c643f008ba2e8a556e25e529c0407a38506555340749559f5100e6fd78cc935bb6c82d2af303beb0d3c6563400659610759b4ed5cb2e0faf36b17e6842f04cdc544c74e051ba")]
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@

using System;
using System.ClientModel.Primitives;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.TypeSpec.Generator.Primitives;
using Microsoft.TypeSpec.Generator.Providers;
using Microsoft.TypeSpec.Generator.Statements;
using static Microsoft.TypeSpec.Generator.Snippets.Snippet;

namespace Microsoft.TypeSpec.Generator.ClientModel.Providers
{
Expand All @@ -22,6 +26,101 @@ protected override TypeSignatureModifiers BuildDeclarationModifiers()

protected override CSharpType[] BuildImplements() => [typeof(ModelReaderWriterContext)];

protected override IReadOnlyList<AttributeStatement> BuildAttributes()
{
var attributes = new List<AttributeStatement>();

// Add ModelReaderWriterBuildableAttribute for all IPersistableModel types
var buildableTypes = CollectBuildableTypes();
foreach (var type in buildableTypes)
{
// Use the full attribute type name to ensure proper compilation
var attributeType = new CSharpType(typeof(ModelReaderWriterBuildableAttribute));
attributes.Add(new AttributeStatement(attributeType, TypeOf(type)));
}

return attributes;
}

/// <summary>
/// Collects all types that implement IPersistableModel, including all models and their properties
/// that are also IPersistableModel types, recursively without duplicates.
/// </summary>
private HashSet<CSharpType> CollectBuildableTypes()
{
var buildableTypes = new HashSet<CSharpType>(new CSharpTypeNameComparer());
var visitedTypes = new HashSet<CSharpType>(new CSharpTypeNameComparer());

// Get all model providers from the output library
var modelProviders = ScmCodeModelGenerator.Instance.OutputLibrary.TypeProviders
.OfType<ModelProvider>()
.ToDictionary(mp => mp.Type, mp => mp, new CSharpTypeNameComparer());

// Process each model recursively
foreach (var modelProvider in modelProviders.Values)
{
CollectBuildableTypesRecursive(modelProvider.Type, buildableTypes, visitedTypes, modelProviders);
}

return buildableTypes;
}

/// <summary>
/// Recursively collects all types that implement IPersistableModel.
/// </summary>
private void CollectBuildableTypesRecursive(CSharpType currentType, HashSet<CSharpType> buildableTypes, HashSet<CSharpType> visitedTypes, Dictionary<CSharpType, ModelProvider> modelProviders)
{
// Avoid infinite recursion by checking if we've already visited this type
if (visitedTypes.Contains(currentType))
{
return;
}

visitedTypes.Add(currentType);

// Check if this type implements IPersistableModel
if (ImplementsIPersistableModel(currentType, modelProviders, out ModelProvider? model))
{
buildableTypes.Add(currentType);

if (model is not null)
{
// Check all properties of this model
foreach (var property in model.Properties)
{
var propertyType = property.Type.IsCollection ? GetInnerMostElement(property.Type) : property.Type;
CollectBuildableTypesRecursive(propertyType, buildableTypes, visitedTypes, modelProviders);
}
}
}
}

private CSharpType GetInnerMostElement(CSharpType type)
{
var result = type.ElementType;
while (result.IsCollection)
{
result = result.ElementType;
}
return result;
}

/// <summary>
/// Checks if a type implements IPersistableModel interface.
/// </summary>
private bool ImplementsIPersistableModel(CSharpType type, Dictionary<CSharpType, ModelProvider> modelProviders, out ModelProvider? model)
{
if (modelProviders.TryGetValue(type, out model))
{
return model.SerializationProviders.OfType<MrwSerializationTypeDefinition>().Any();
}

if (!type.IsFrameworkType || type.IsEnum || type.IsLiteral)
return false;

return type.FrameworkType.GetInterfaces().Any(i => i.Name == "IPersistableModel`1" || i.Name == "IJsonModel`1");
}

protected override XmlDocProvider BuildXmlDocs()
{
var summary = new Statements.XmlDocSummaryStatement(
Expand Down Expand Up @@ -49,5 +148,29 @@ private static string RemovePeriods(string input)

return buffer.Slice(0, index).ToString();
}

private class CSharpTypeNameComparer : IEqualityComparer<CSharpType>
{
public bool Equals(CSharpType? x, CSharpType? y)
{
if (x is null && y is null)
{
return true;
}
if (x is null || y is null)
{
return false;
}
return x.Namespace == y.Namespace && x.Name == y.Name;
}

public int GetHashCode(CSharpType obj)
{
HashCode hashCode = new HashCode();
hashCode.Add(obj.Namespace);
hashCode.Add(obj.Name);
return hashCode.ToHashCode();
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("Microsoft.TypeSpec.Generator.Tests.Perf, PublicKey=002400000480000094000000060200000024000052534131000400000100010041df4fe80c5af6ff9a410db5a173b0ce24ad68764c623e308b1584a88b1d1d82277f746c1cccba48997e13db3366d5ed676576ffd293293baf42c643f008ba2e8a556e25e529c0407a38506555340749559f5100e6fd78cc935bb6c82d2af303beb0d3c6563400659610759b4ed5cb2e0faf36b17e6842f04cdc544c74e051ba")]
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.ClientModel.Primitives;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using Microsoft.TypeSpec.Generator.ClientModel.Providers;
using Microsoft.TypeSpec.Generator.Input;
using Microsoft.TypeSpec.Generator.Primitives;
using Microsoft.TypeSpec.Generator.Tests.Common;
using NUnit.Framework;

namespace Microsoft.TypeSpec.Generator.ClientModel.Tests.Providers.Definitions
{
public class ModelReaderWriterContextDefinitionTests
{
[Test]
public void ValidateModelReaderWriterContextIsGenerated()
{
MockHelpers.LoadMockGenerator();

var contextDefinition = new ModelReaderWriterContextDefinition();

Assert.IsNotNull(contextDefinition);
Assert.IsNotNull(contextDefinition.Name);
Assert.IsTrue(contextDefinition.Name.EndsWith("Context"));
Assert.IsNotNull(contextDefinition.DeclarationModifiers);
Assert.IsTrue(contextDefinition.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public));
Assert.IsTrue(contextDefinition.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Partial));
Assert.IsTrue(contextDefinition.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Class));
Assert.IsNotNull(contextDefinition.Implements);
Assert.IsTrue(contextDefinition.Implements.Contains(typeof(ModelReaderWriterContext)));
}

[Test]
public void ValidateModelReaderWriterBuildableAttributesAreGenerated()
{
var mockGenerator = MockHelpers.LoadMockGenerator(
inputModels: () => new List<InputModelType>
{
InputFactory.Model("TestModel", properties:
[
InputFactory.Property("StringProperty", InputPrimitiveType.String),
InputFactory.Property("IntProperty", InputPrimitiveType.Int32)
])
});

var contextDefinition = new ModelReaderWriterContextDefinition();
var attributes = contextDefinition.Attributes;

Assert.IsNotNull(attributes);
Assert.IsTrue(attributes.Count > 0);

// Check that exactly one ModelReaderWriterBuildableAttribute exists since TestModel has only primitive properties
var buildableAttributes = attributes.Where(a => a.Type.IsFrameworkType && a.Type.FrameworkType == typeof(ModelReaderWriterBuildableAttribute));
Assert.AreEqual(1, buildableAttributes.Count(), "Exactly one ModelReaderWriterBuildableAttribute should be generated for TestModel");
}

[Test]
public void ValidateModelReaderWriterBuildableAttributesIncludeNestedModels()
{
// Create a model with a property that references another model
var nestedModel = InputFactory.Model("NestedModel", properties:
[
InputFactory.Property("NestedValue", InputPrimitiveType.String)
]);

var parentModel = InputFactory.Model("ParentModel", properties:
[
InputFactory.Property("NestedProperty", nestedModel),
InputFactory.Property("SimpleProperty", InputPrimitiveType.String)
]);

var mockGenerator = MockHelpers.LoadMockGenerator(
inputModels: () => [parentModel, nestedModel]);

var contextDefinition = new ModelReaderWriterContextDefinition();
var attributes = contextDefinition.Attributes;

Assert.IsNotNull(attributes);
Assert.IsTrue(attributes.Count > 0);

// Check that exactly two ModelReaderWriterBuildableAttribute exist for both models
var buildableAttributes = attributes.Where(a => a.Type.IsFrameworkType && a.Type.FrameworkType == typeof(ModelReaderWriterBuildableAttribute));
Assert.AreEqual(2, buildableAttributes.Count(), "Exactly two ModelReaderWriterBuildableAttributes should be generated for nested models");
}

[Test]
public void ValidateModelReaderWriterBuildableAttributesHandleCollectionProperties()
{
// Create a model with a collection property containing another model
var itemModel = InputFactory.Model("ItemModel", properties:
[
InputFactory.Property("ItemValue", InputPrimitiveType.String)
]);

var collectionModel = InputFactory.Model("CollectionModel", properties:
[
InputFactory.Property("Items", InputFactory.Array(itemModel))
]);

var mockGenerator = MockHelpers.LoadMockGenerator(
inputModels: () => [collectionModel, itemModel]);

var contextDefinition = new ModelReaderWriterContextDefinition();
var attributes = contextDefinition.Attributes;

Assert.IsNotNull(attributes);
Assert.IsTrue(attributes.Count > 0);

// Check that exactly two ModelReaderWriterBuildableAttribute exist for both models
var buildableAttributes = attributes.Where(a => a.Type.IsFrameworkType && a.Type.FrameworkType == typeof(ModelReaderWriterBuildableAttribute));
Assert.AreEqual(2, buildableAttributes.Count(), "Exactly two ModelReaderWriterBuildableAttributes should be generated for collection item models");
}

[Test]
public void ValidateModelReaderWriterBuildableAttributesAvoidDuplicates()
{
// Create models with circular references to test duplicate handling
var modelA = InputFactory.Model("ModelA", properties:
[
InputFactory.Property("PropertyA", InputPrimitiveType.String)
]);

var modelB = InputFactory.Model("ModelB", properties:
[
InputFactory.Property("PropertyB", InputPrimitiveType.String),
InputFactory.Property("ModelARef", modelA)
]);

// Add a property to ModelA that references ModelB to create a circular reference
var modelAWithCircularRef = InputFactory.Model("ModelA", properties:
[
InputFactory.Property("PropertyA", InputPrimitiveType.String),
InputFactory.Property("ModelBRef", modelB)
]);

var mockGenerator = MockHelpers.LoadMockGenerator(
inputModels: () => [modelAWithCircularRef, modelB]);

var contextDefinition = new ModelReaderWriterContextDefinition();
var attributes = contextDefinition.Attributes;

Assert.IsNotNull(attributes);

// Check that no duplicate attributes exist
var buildableAttributes = attributes.Where(a => a.Type.IsFrameworkType && a.Type.FrameworkType == typeof(ModelReaderWriterBuildableAttribute));
var uniqueTypes = buildableAttributes.Select(a => a.Arguments.First().ToString()).Distinct().ToList();

Assert.AreEqual(buildableAttributes.Count(), uniqueTypes.Count,
"No duplicate ModelReaderWriterBuildableAttributes should be generated");
}

[Test]
public void ValidateModelReaderWriterBuildableAttributesIncludeDependencyModels()
{
// Create a model with a property that references a model from a dependency library
// The dependency model won't have a model provider in the current library
var dependencyModel = InputFactory.Model("DependencyModel");

var parentModel = InputFactory.Model("ParentModel", properties:
[
InputFactory.Property("DependencyProperty", dependencyModel),
InputFactory.Property("SimpleProperty", InputPrimitiveType.String)
]);

// Only include the parentModel in the mock generator, simulating that
// dependencyModel is from a dependency library
var mockGenerator = MockHelpers.LoadMockGenerator(
inputModels: () => [parentModel],
createCSharpTypeCore: input =>
{
return new CSharpType(typeof(DependencyModel));
},
createCSharpTypeCoreFallback: input => input.Name == "DependencyModel");

var contextDefinition = new ModelReaderWriterContextDefinition();
var attributes = contextDefinition.Attributes;

Assert.IsNotNull(attributes);
Assert.IsTrue(attributes.Count > 0);

// Check that exactly two ModelReaderWriterBuildableAttribute exist:
// one for ParentModel and one for the dependency model
var buildableAttributes = attributes.Where(a => a.Type.IsFrameworkType && a.Type.FrameworkType == typeof(ModelReaderWriterBuildableAttribute));
Assert.AreEqual(2, buildableAttributes.Count(), "Exactly two ModelReaderWriterBuildableAttributes should be generated for models with dependency references");
}

private class DependencyModel : IJsonModel<DependencyModel>
{
DependencyModel? IJsonModel<DependencyModel>.Create(ref Utf8JsonReader reader, ModelReaderWriterOptions options)
{
throw new NotImplementedException();
}

DependencyModel? IPersistableModel<DependencyModel>.Create(BinaryData data, ModelReaderWriterOptions options)
{
throw new NotImplementedException();
}

string IPersistableModel<DependencyModel>.GetFormatFromOptions(ModelReaderWriterOptions options)
{
throw new NotImplementedException();
}

void IJsonModel<DependencyModel>.Write(Utf8JsonWriter writer, ModelReaderWriterOptions options)
{
throw new NotImplementedException();
}

BinaryData IPersistableModel<DependencyModel>.Write(ModelReaderWriterOptions options)
{
throw new NotImplementedException();
}
}
}
}
Loading
Loading