Skip to content

Commit f97406c

Browse files
Chris Martinezcommonsensesoftware
Chris Martinez
authored andcommitted
Support self-referencing type substitution. Related to #382
1 parent e586283 commit f97406c

File tree

10 files changed

+299
-30
lines changed

10 files changed

+299
-30
lines changed

src/Common.OData.ApiExplorer/AspNet.OData/ClassProperty.cs

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,16 @@
1111

1212
struct ClassProperty
1313
{
14+
readonly Type type;
1415
internal readonly string Name;
15-
internal readonly Type Type;
1616

1717
internal ClassProperty( PropertyInfo clrProperty, Type propertyType )
1818
{
1919
Contract.Requires( clrProperty != null );
2020
Contract.Requires( propertyType != null );
2121

2222
Name = clrProperty.Name;
23-
Type = propertyType;
23+
type = propertyType;
2424
Attributes = AttributesFromProperty( clrProperty );
2525
}
2626

@@ -30,12 +30,36 @@ internal ClassProperty( IEnumerable<Assembly> assemblies, IEdmOperationParameter
3030
Contract.Requires( parameter != null );
3131

3232
Name = parameter.Name;
33-
Type = parameter.Type.Definition.GetClrType( assemblies );
33+
type = parameter.Type.Definition.GetClrType( assemblies );
3434
Attributes = AttributesFromOperationParameter( parameter );
3535
}
3636

3737
internal IEnumerable<CustomAttributeBuilder> Attributes { get; }
3838

39+
public override int GetHashCode() => ( Name.GetHashCode() * 397 ) ^ type.GetHashCode();
40+
41+
public Type GetType( Type declaringType )
42+
{
43+
Contract.Requires( declaringType != null );
44+
Contract.Ensures( Contract.Result<Type>() != null );
45+
46+
if ( type == DeclaringType.Value )
47+
{
48+
return declaringType;
49+
}
50+
else if ( type.IsGenericType )
51+
{
52+
var typeArgs = type.GetGenericArguments();
53+
54+
if ( typeArgs.Length == 1 && typeArgs[0] == DeclaringType.Value )
55+
{
56+
return type.GetGenericTypeDefinition().MakeGenericType( declaringType );
57+
}
58+
}
59+
60+
return type;
61+
}
62+
3963
static IEnumerable<CustomAttributeBuilder> AttributesFromProperty( PropertyInfo clrProperty )
4064
{
4165
Contract.Requires( clrProperty != null );
@@ -93,7 +117,5 @@ static IEnumerable<CustomAttributeBuilder> AttributesFromOperationParameter( IEd
93117

94118
yield return new CustomAttributeBuilder( ctor, args );
95119
}
96-
97-
public override int GetHashCode() => ( Name.GetHashCode() * 397 ) ^ Type.GetHashCode();
98120
}
99121
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace Microsoft.AspNet.OData
2+
{
3+
using System;
4+
5+
struct DeclaringType
6+
{
7+
internal static Type Value = typeof( DeclaringType );
8+
}
9+
}

src/Common.OData.ApiExplorer/AspNet.OData/DefaultModelTypeBuilder.cs

Lines changed: 71 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ public sealed class DefaultModelTypeBuilder : IModelTypeBuilder
3030
readonly ICollection<Assembly> assemblies;
3131
readonly ConcurrentDictionary<ApiVersion, ModuleBuilder> modules = new ConcurrentDictionary<ApiVersion, ModuleBuilder>();
3232
readonly ConcurrentDictionary<ClassSignature, Type> generatedTypes = new ConcurrentDictionary<ClassSignature, Type>();
33+
readonly Dictionary<EdmTypeKey, Type> visitedEdmTypes = new Dictionary<EdmTypeKey, Type>();
3334

3435
/// <summary>
3536
/// Initializes a new instance of the <see cref="DefaultModelTypeBuilder"/> class.
@@ -49,6 +50,13 @@ public Type NewStructuredType( IEdmStructuredType structuredType, Type clrType,
4950
Arg.NotNull( apiVersion, nameof( apiVersion ) );
5051
Contract.Ensures( Contract.Result<Type>() != null );
5152

53+
var typeKey = new EdmTypeKey( structuredType, apiVersion );
54+
55+
if ( visitedEdmTypes.TryGetValue( typeKey, out var generatedType ) )
56+
{
57+
return generatedType;
58+
}
59+
5260
const BindingFlags bindingFlags = BindingFlags.Public | BindingFlags.Instance;
5361

5462
var properties = new List<ClassProperty>();
@@ -63,33 +71,19 @@ public Type NewStructuredType( IEdmStructuredType structuredType, Type clrType,
6371
continue;
6472
}
6573

66-
var propertyType = property.PropertyType;
6774
var structuredTypeRef = structuralProperty.Type;
75+
var propertyType = property.PropertyType;
6876

6977
if ( structuredTypeRef.IsCollection() )
7078
{
71-
var collectionType = structuredTypeRef.AsCollection();
72-
var elementType = collectionType.ElementType();
73-
74-
if ( elementType.IsStructured() )
75-
{
76-
assemblies.Add( clrType.Assembly );
77-
78-
var itemType = elementType.Definition.GetClrType( assemblies );
79-
var newItemType = NewStructuredType( elementType.ToStructuredType(), itemType, apiVersion );
80-
81-
if ( !itemType.Equals( newItemType ) )
82-
{
83-
propertyType = IEnumerableOfT.MakeGenericType( newItemType );
84-
}
85-
}
79+
propertyType = NewStructuredTypeOrSelf( typeKey, structuredTypeRef.AsCollection(), propertyType, apiVersion );
8680
}
8781
else if ( structuredTypeRef.IsStructured() )
8882
{
89-
propertyType = NewStructuredType( structuredTypeRef.ToStructuredType(), property.PropertyType, apiVersion );
83+
propertyType = NewStructuredTypeOrSelf( typeKey, structuredTypeRef.ToStructuredType(), propertyType, apiVersion );
9084
}
9185

92-
clrTypeMatchesEdmType &= property.PropertyType.Equals( propertyType );
86+
clrTypeMatchesEdmType &= propertyType.IsDeclaringType() || property.PropertyType.Equals( propertyType );
9387
properties.Add( new ClassProperty( property, propertyType ) );
9488
}
9589

@@ -100,7 +94,10 @@ public Type NewStructuredType( IEdmStructuredType structuredType, Type clrType,
10094

10195
var signature = new ClassSignature( clrType.FullName, properties, apiVersion );
10296

103-
return generatedTypes.GetOrAdd( signature, CreateFromSignature );
97+
generatedType = generatedTypes.GetOrAdd( signature, CreateFromSignature );
98+
visitedEdmTypes.Add( typeKey, generatedType );
99+
100+
return generatedType;
104101
}
105102

106103
/// <inheritdoc />
@@ -128,10 +125,12 @@ TypeInfo CreateFromSignature( ClassSignature @class )
128125

129126
foreach ( var property in @class.Properties )
130127
{
131-
var field = typeBuilder.DefineField( "_" + property.Name, property.Type, FieldAttributes.Private );
132-
var propertyBuilder = typeBuilder.DefineProperty( property.Name, PropertyAttributes.HasDefault, property.Type, null );
133-
var getter = typeBuilder.DefineMethod( "get_" + property.Name, PropertyMethodAttributes, property.Type, Type.EmptyTypes );
134-
var setter = typeBuilder.DefineMethod( "set_" + property.Name, PropertyMethodAttributes, null, new[] { property.Type } );
128+
var type = property.GetType( typeBuilder );
129+
var name = property.Name;
130+
var field = typeBuilder.DefineField( "_" + name, type, FieldAttributes.Private );
131+
var propertyBuilder = typeBuilder.DefineProperty( name, PropertyAttributes.HasDefault, type, null );
132+
var getter = typeBuilder.DefineMethod( "get_" + name, PropertyMethodAttributes, type, Type.EmptyTypes );
133+
var setter = typeBuilder.DefineMethod( "set_" + name, PropertyMethodAttributes, null, new[] { type } );
135134
var il = getter.GetILGenerator();
136135

137136
il.Emit( OpCodes.Ldarg_0 );
@@ -156,6 +155,55 @@ TypeInfo CreateFromSignature( ClassSignature @class )
156155
return typeBuilder.CreateTypeInfo();
157156
}
158157

158+
Type NewStructuredTypeOrSelf( EdmTypeKey declaringTypeKey, IEdmCollectionTypeReference collectionType, Type clrType, ApiVersion apiVersion )
159+
{
160+
Contract.Requires( collectionType != null );
161+
Contract.Requires( clrType != null );
162+
Contract.Requires( apiVersion != null );
163+
Contract.Ensures( Contract.Result<Type>() != null );
164+
165+
var elementType = collectionType.ElementType();
166+
167+
if ( !elementType.IsStructured() )
168+
{
169+
return clrType;
170+
}
171+
172+
var structuredType = elementType.ToStructuredType();
173+
174+
if ( declaringTypeKey == new EdmTypeKey( structuredType, apiVersion ) )
175+
{
176+
return IEnumerableOfT.MakeGenericType( DeclaringType.Value );
177+
}
178+
179+
assemblies.Add( clrType.Assembly );
180+
181+
var itemType = elementType.Definition.GetClrType( assemblies );
182+
var newItemType = NewStructuredType( structuredType, itemType, apiVersion );
183+
184+
if ( !itemType.Equals( newItemType ) )
185+
{
186+
clrType = IEnumerableOfT.MakeGenericType( newItemType );
187+
}
188+
189+
return clrType;
190+
}
191+
192+
Type NewStructuredTypeOrSelf( EdmTypeKey declaringTypeKey, IEdmStructuredType structuredType, Type clrType, ApiVersion apiVersion )
193+
{
194+
Contract.Requires( structuredType != null );
195+
Contract.Requires( clrType != null );
196+
Contract.Requires( apiVersion != null );
197+
Contract.Ensures( Contract.Result<Type>() != null );
198+
199+
if ( declaringTypeKey == new EdmTypeKey( structuredType, apiVersion ) )
200+
{
201+
return DeclaringType.Value;
202+
}
203+
204+
return NewStructuredType( structuredType, clrType, apiVersion );
205+
}
206+
159207
static ModuleBuilder CreateModuleForApiVersion( ApiVersion apiVersion )
160208
{
161209
var name = new AssemblyName( $"T{NewGuid().ToString( "n", InvariantCulture )}.DynamicModels" );
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
namespace Microsoft.AspNet.OData
2+
{
3+
#if !WEBAPI
4+
using Microsoft.AspNetCore.Mvc;
5+
#endif
6+
using Microsoft.OData.Edm;
7+
#if WEBAPI
8+
using Microsoft.Web.Http;
9+
#endif
10+
using System;
11+
using System.Diagnostics.Contracts;
12+
13+
struct EdmTypeKey : IEquatable<EdmTypeKey>
14+
{
15+
readonly int hashCode;
16+
17+
internal EdmTypeKey( IEdmStructuredType type, ApiVersion apiVersion )
18+
{
19+
Contract.Requires( type != null );
20+
Contract.Requires( apiVersion != null );
21+
22+
hashCode = ComputeHash( type.FullTypeName(), apiVersion );
23+
}
24+
25+
internal EdmTypeKey( IEdmTypeReference type, ApiVersion apiVersion )
26+
{
27+
Contract.Requires( type != null );
28+
Contract.Requires( apiVersion != null );
29+
30+
hashCode = ComputeHash( type.FullName(), apiVersion );
31+
}
32+
33+
public static bool operator ==( EdmTypeKey obj, EdmTypeKey other ) => obj.Equals( other );
34+
35+
public static bool operator !=( EdmTypeKey obj, EdmTypeKey other ) => !obj.Equals( other );
36+
37+
public override int GetHashCode() => hashCode;
38+
39+
public override bool Equals( object obj ) => obj is EdmTypeKey other && Equals( other );
40+
41+
public bool Equals( EdmTypeKey other ) => hashCode == other.hashCode;
42+
43+
static int ComputeHash( string fullName, ApiVersion apiVersion )
44+
{
45+
Contract.Requires( !string.IsNullOrEmpty( fullName ) );
46+
Contract.Requires( apiVersion != null );
47+
48+
return ( fullName.GetHashCode() * 397 ) ^ apiVersion.GetHashCode();
49+
}
50+
}
51+
}

src/Common.OData.ApiExplorer/AspNet.OData/TypeExtensions.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,25 @@ internal static void Deconstruct<T1, T2>( this Tuple<T1, T2> tuple, out T1 item1
8282
item2 = tuple.Item2;
8383
}
8484

85+
internal static bool IsDeclaringType( this Type type )
86+
{
87+
Contract.Requires( type != null );
88+
89+
if ( type == DeclaringType.Value )
90+
{
91+
return true;
92+
}
93+
94+
if ( !type.IsGenericType )
95+
{
96+
return false;
97+
}
98+
99+
var typeArgs = type.GetGenericArguments();
100+
101+
return typeArgs.Length == 1 && typeArgs[0] == DeclaringType.Value;
102+
}
103+
85104
static bool IsSubstitutableGeneric( Type type, Stack<Type> openTypes, out Type innerType )
86105
{
87106
Contract.Requires( type != null );

src/Common.OData.ApiExplorer/Common.OData.ApiExplorer.projitems

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
<ItemGroup>
1212
<Compile Include="$(MSBuildThisFileDirectory)AspNet.OData\ClassProperty.cs" />
1313
<Compile Include="$(MSBuildThisFileDirectory)AspNet.OData\ClassSignature.cs" />
14+
<Compile Include="$(MSBuildThisFileDirectory)AspNet.OData\DeclaringType.cs" />
15+
<Compile Include="$(MSBuildThisFileDirectory)AspNet.OData\EdmTypeKey.cs" />
1416
<Compile Include="$(MSBuildThisFileDirectory)AspNet.OData\IModelTypeBuilder.cs" />
1517
<Compile Include="$(MSBuildThisFileDirectory)AspNet.OData\DefaultModelTypeBuilder.cs" />
1618
<Compile Include="$(MSBuildThisFileDirectory)AspNet.OData\ODataValue{T}.cs" />
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
namespace Microsoft.AspNet.OData
2+
{
3+
using System;
4+
using System.Collections.Generic;
5+
6+
public class Company
7+
{
8+
public int CompanyId { get; set; }
9+
10+
public Company ParentCompany { get; set; }
11+
12+
public List<Company> Subsidiaries { get; set; }
13+
14+
public string Name { get; set; }
15+
16+
public DateTime DateFounded { get; set; }
17+
}
18+
}

test/Microsoft.AspNet.OData.Versioning.ApiExplorer.Tests/AspNet.OData/DefaultModelTypeBuilderTest.cs

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,47 @@ public void type_should_match_edm_with_nested_entity_substitution()
132132
innerType.Should().HaveProperty<string>( nameof( Contact.LastName ) );
133133
}
134134

135+
[Fact]
136+
public void type_should_match_with_self_referencing_property_substitution()
137+
{
138+
// arrange
139+
var modelBuilder = new ODataConventionModelBuilder();
140+
141+
modelBuilder.EntitySet<Company>( "Companies" );
142+
143+
var context = NewContext( modelBuilder.GetEdmModel() );
144+
var originalType = typeof( Company );
145+
146+
//act
147+
var subsitutedType = originalType.SubstituteIfNecessary( context );
148+
149+
// assert
150+
subsitutedType.Should().Be( typeof( Company ) );
151+
}
152+
153+
[Fact]
154+
public void type_should_use_self_referencing_property_substitution()
155+
{
156+
// arrange
157+
var modelBuilder = new ODataConventionModelBuilder();
158+
var company = modelBuilder.EntitySet<Company>( "Companies" ).EntityType;
159+
160+
company.Ignore( c => c.DateFounded );
161+
162+
var context = NewContext( modelBuilder.GetEdmModel() );
163+
var originalType = typeof( Company );
164+
165+
//act
166+
var subsitutedType = originalType.SubstituteIfNecessary( context );
167+
168+
// assert
169+
subsitutedType.GetRuntimeProperties().Should().HaveCount( 4 );
170+
subsitutedType.Should().HaveProperty<int>( nameof( Company.CompanyId ) );
171+
subsitutedType.Should().HaveProperty<string>( nameof( Company.Name ) );
172+
subsitutedType.Should().Be( subsitutedType.GetRuntimeProperty( nameof( Company.ParentCompany ) ).PropertyType );
173+
subsitutedType.Should().Be( subsitutedType.GetRuntimeProperty( nameof( Company.Subsidiaries ) ).PropertyType.GetGenericArguments()[0] );
174+
}
175+
135176
[Theory]
136177
[MemberData( nameof( SubstitutionData ) )]
137178
public void type_should_match_edm_with_child_entity_substitution( Type originalType )
@@ -158,7 +199,7 @@ public void type_should_match_edm_with_child_entity_substitution( Type originalT
158199
nextType.Should().HaveProperty<string>( nameof( Contact.LastName ) );
159200
nextType.Should().HaveProperty<string>( nameof( Contact.Email ) );
160201
nextType.Should().HaveProperty<string>( nameof( Contact.Phone ) );
161-
nextType = nextType.GetRuntimeProperties().Single( p => p.Name == nameof( Contact.Addresses ) ).PropertyType.GetGenericArguments()[0];
202+
nextType = nextType.GetRuntimeProperty( nameof( Contact.Addresses ) ).PropertyType.GetGenericArguments()[0];
162203
nextType.GetRuntimeProperties().Should().HaveCount( 5 );
163204
nextType.Should().HaveProperty<int>( nameof( Address.AddressId ) );
164205
nextType.Should().HaveProperty<string>( nameof( Address.Street ) );

0 commit comments

Comments
 (0)