Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -370,21 +370,52 @@ private List<PropertyProvider> BuildAdditionalPropertyProperties()
var type = !_inputModel.Usage.HasFlag(InputModelTypeUsage.Input)
? additionalPropsType.OutputType
: additionalPropsType;
var assignment = type.IsReadOnlyDictionary
? new ExpressionPropertyBody(New.ReadOnlyDictionary(type.Arguments[0], type.ElementType, RawDataField))
: new ExpressionPropertyBody(RawDataField);
var property = new PropertyProvider(
null,
MethodSignatureModifiers.Public,
type,
name,
assignment,
this)

// Check for backward compatibility: if LastContract has an object-typed AdditionalProperties
var hasObjectAdditionalPropertiesInLastContract = CheckForObjectAdditionalPropertiesInLastContract(name);
if (hasObjectAdditionalPropertiesInLastContract)
{
BackingField = RawDataField,
IsAdditionalProperties = true
};
properties.Add(property);
// Generate the object-typed property for backward compatibility
var objectDictType = new CSharpType(typeof(IDictionary<,>), typeof(string), typeof(object));
var objectType = !_inputModel.Usage.HasFlag(InputModelTypeUsage.Input)
? objectDictType.OutputType
: objectDictType;
var objectAssignment = objectType.IsReadOnlyDictionary
? new ExpressionPropertyBody(New.ReadOnlyDictionary(objectType.Arguments[0], objectType.ElementType, RawDataField))
: new ExpressionPropertyBody(RawDataField);
var objectProperty = new PropertyProvider(
null,
MethodSignatureModifiers.Public,
objectType,
name,
objectAssignment,
this)
{
BackingField = RawDataField,
IsAdditionalProperties = true
};
properties.Add(objectProperty);

// Don't add the BinaryData property to avoid conflict
}
else
{
var assignment = type.IsReadOnlyDictionary
? new ExpressionPropertyBody(New.ReadOnlyDictionary(type.Arguments[0], type.ElementType, RawDataField))
: new ExpressionPropertyBody(RawDataField);
var property = new PropertyProvider(
null,
MethodSignatureModifiers.Public,
type,
name,
assignment,
this)
{
BackingField = RawDataField,
IsAdditionalProperties = true
};
properties.Add(property);
}
}

return properties;
Expand Down Expand Up @@ -1175,6 +1206,38 @@ _ when type.Equals(_additionalPropsUnknownType, ignoreNullable: true) => type,
};
}

private bool CheckForObjectAdditionalPropertiesInLastContract(string propertyName)
{
if (LastContractView == null)
{
return false;
}

// Check if the property exists in the last contract by name (may not have IsAdditionalProperties set)
var lastContractProperty = LastContractView.Properties.FirstOrDefault(p => p.Name == propertyName);

if (lastContractProperty == null)
{
return false;
}

// Check if it's IDictionary<string, object>
var propertyType = lastContractProperty.Type;
if (propertyType.IsDictionary && propertyType.Arguments.Count == 2)
{
var keyType = propertyType.Arguments[0];
var valueType = propertyType.Arguments[1];

// Check if key is string and value is object
if (keyType.FrameworkType == typeof(string) && valueType.FrameworkType == typeof(object))
{
return true;
}
}

return false;
}

private static string BuildAdditionalTypePropertiesFieldName(CSharpType additionalPropertiesValueType)
{
var name = additionalPropertiesValueType.Name;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1807,5 +1807,37 @@ await MockHelpers.LoadMockGeneratorAsync(
Assert.IsNotNull(publicConstructor, "Constructor modifier should be changed to public for backward compatibility");
Assert.AreEqual("baseProp", publicConstructor!.Signature.Parameters[0].Name);
}

[Test]
public async Task TestBuildProperties_WithObjectAdditionalPropertiesBackwardCompatibility()
Comment thread
JoshLove-msft marked this conversation as resolved.
{
// Create a model with unknown additional properties (which would normally generate BinaryData)
var inputModel = InputFactory.Model(
"TestModel",
usage: InputModelTypeUsage.Input,
properties: [InputFactory.Property("Name", InputPrimitiveType.String, isRequired: true)],
additionalProperties: InputPrimitiveType.Any);

await MockHelpers.LoadMockGeneratorAsync(
inputModelTypes: [inputModel],
lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync());

var modelProvider = CodeModelGenerator.Instance.TypeFactory.CreateModel(inputModel);

Assert.IsNotNull(modelProvider);
Assert.IsNotNull(modelProvider!.Properties);

// Verify that AdditionalProperties property exists and has object type for backward compatibility
var additionalPropertiesProperty = modelProvider.Properties.FirstOrDefault(p => p.Name == "AdditionalProperties");
Assert.IsNotNull(additionalPropertiesProperty, "AdditionalProperties property should be generated");
Assert.IsTrue(additionalPropertiesProperty!.IsAdditionalProperties, "Property should be marked as additional properties");

// Verify the type is IDictionary<string, object> for backward compatibility
var propertyType = additionalPropertiesProperty.Type;
Assert.IsTrue(propertyType.IsDictionary, "Property should be a dictionary type");
Assert.AreEqual(2, propertyType.Arguments.Count, "Dictionary should have 2 type arguments");
Assert.AreEqual(typeof(string), propertyType.Arguments[0].FrameworkType, "Key type should be string");
Assert.AreEqual(typeof(object), propertyType.Arguments[1].FrameworkType, "Value type should be object for backward compatibility");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;

namespace Sample.Models
{
public partial class TestModel
{
public TestModel(string name, IDictionary<string, object> additionalProperties)
{
Name = name;
AdditionalProperties = additionalProperties;
}

public string Name { get; set; }
public IDictionary<string, object> AdditionalProperties { get; }
}
}
Loading