Skip to content

Support self referencing type substitution #382

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

Closed

Conversation

LuukN2
Copy link

@LuukN2 LuukN2 commented Oct 24, 2018

Thanks for the substitution fixes.

However, there was still a small recursion issue when a model references itself on a property. (Or when two models reference each other, etc.)
The program would get stuck in a loop getting the structured type of the properties causing a stackoverflow exception.
Now it keeps track of what types it has already substituted in a dictionary and passes these values instead of recreating the structured type again.

Thanks again for your hard work, your project is awesome!

@msftclas
Copy link

msftclas commented Oct 24, 2018

CLA assistant check
All CLA requirements met.

@commonsensesoftware
Copy link
Collaborator

This is great! I suspected there were still some edge cases I was missing. This is a really convenient feature to use, but it's not so easy to implement. Let me go through this and we'll get it merged.

continue;
}

passedTypes.Add(structuredTypeRef.ToString(), propertyType);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use FullName() here instead of ToString() (unless you know better). It looks like the implementation of ToString is ToTraceString, which may not be the same. IMO FullName() also gives better semantic meaning to what the code is doing here.

@@ -66,6 +67,14 @@ public Type NewStructuredType( IEdmStructuredType structuredType, Type clrType,
var propertyType = property.PropertyType;
var structuredTypeRef = structuralProperty.Type;

if ( passedTypes.TryGetValue( structuredTypeRef.ToString(), out var passedType ) )
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's capture the type name so we don't have to build it two times. Something like:

var typeName = structuredTypeRef.FullName();

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This obviously supplanted by the struct key or hash code if you refactor

@@ -30,6 +30,7 @@ public sealed class DefaultModelTypeBuilder : IModelTypeBuilder
readonly ICollection<Assembly> assemblies;
readonly ConcurrentDictionary<ApiVersion, ModuleBuilder> modules = new ConcurrentDictionary<ApiVersion, ModuleBuilder>();
readonly ConcurrentDictionary<ClassSignature, Type> generatedTypes = new ConcurrentDictionary<ClassSignature, Type>();
private readonly Dictionary<string, Type> passedTypes = new Dictionary<string, Type>();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: let's use visitedTypes as it's more consistent with the pattern being used

@@ -66,6 +67,14 @@ public Type NewStructuredType( IEdmStructuredType structuredType, Type clrType,
var propertyType = property.PropertyType;
var structuredTypeRef = structuralProperty.Type;

if ( passedTypes.TryGetValue( structuredTypeRef.ToString(), out var passedType ) )
{
properties.Add(new ClassProperty(property, passedType));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: the spacing is off by my rules, but this should be flagged by code analysis and auto-formatted by the .editorconfig settings. correcting this should be as easy as formatting the document.

Definitely let me know you don't see CA warnings and/or formatting the document doesn't fix all of these. You're not supposed to have to guess or do a lot of work to format things the way I want them to be. ;)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It formats for methods, but I had to manually add the formatting rules for nameof, typeof etc.
I'm using Jetbrains Rider if that means anything.


// assert
subsitutedType.GetRuntimeProperties().Should().HaveCount( 3 );
subsitutedType.Should().HaveProperty<int>( nameof(Company.CompanyId) );
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: spacing; format of the document should fix them all

var person = modelBuilder.EntitySet<Company>( "Companies" ).EntityType;

var context = NewContext( modelBuilder.GetEdmModel() );
var originalType = typeof(Company);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: spacing; format of the document should fix them all

@@ -1,23 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file doesn't seem to have actually been changed. It appears that only the extra lines have been removed. It's not a huge deal, but there's not much sense in changing a file that doesn't need changing.

@@ -30,6 +30,7 @@ public sealed class DefaultModelTypeBuilder : IModelTypeBuilder
readonly ICollection<Assembly> assemblies;
readonly ConcurrentDictionary<ApiVersion, ModuleBuilder> modules = new ConcurrentDictionary<ApiVersion, ModuleBuilder>();
readonly ConcurrentDictionary<ClassSignature, Type> generatedTypes = new ConcurrentDictionary<ClassSignature, Type>();
private readonly Dictionary<string, Type> passedTypes = new Dictionary<string, Type>();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In thinking about this, I'm pretty sure that using string alone for the key is not going to work because there can be a distinctly generated type by API version. This should mean that the key for the corresponding EDM type should be a combination of EDM type and API version; otherwise, it's possible that the wrong generated type will be picked.

For example, imagine that Default.Company is generated for API version 2.0 and then it's requested again for API version 1.0. If the models aren't exactly the same (which they probably aren't), the wrong selection is made.

There's two ways this can be handled:

  1. Add a hashable struct, something like VisitedTypeKey
  2. Use int as the key and add a hashing function

Option 2 might look like:

static int GetVisitedTypeKey( IEdmTypeReference type, ApiVersion apiVersion ) =>
  ( type.FullName().GetHashCode() * 397 ) ^ apiVersion.GetHashCode();

continue;
}

passedTypes.Add(structuredTypeRef.ToString(), propertyType);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding the visited type needs to be added toward the end; probably after line 102. The propertyType variable can be mutated after this line, which means the wrong type will be stored in the dictionary.

@commonsensesoftware
Copy link
Collaborator

Thanks @LuukN2 for your contribution; really - it was useful. I'm going to close this one out because it turns out there's quite a bit more going on than just these rather basic fixes. Your change does address the basic recursion problem, but it completely misses recursively emitting a self-referencing type when it's actually substituted. I realized that this was missing when incorporating your unit tests. I even had to do some research because I had no idea how to actually build a type like that. I also expanded your scenario to also include the property List<Company> Subsidiaries { get; }, which should be substituted as IEnumerable<TDeclaringType> Subsidiaries { get; } when required.

I've incorporated your bits and all the additional required changes into #384 that I already had going. The last commit has all of the changes relevant to this PR. I'll leave #384 open for a bit so you have some time to review and comment if you want.

Thanks again for this submission. I don't think I would have remembered about this use case otherwise.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants