-
Notifications
You must be signed in to change notification settings - Fork 468
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
Fix attribute comparison for ProxyGenerator cache #219
Fix attribute comparison for ProxyGenerator cache #219
Conversation
@thomaslevesque thanks, it looks good, sorry for the slow response to review this. I do like FromExpression, much better than the now obsolete code that pulls apart an actual Attribute instance to try rebuilding it. Now to discuss whether this should be a breaking change. Looking at the 3 main mocking libraries only FakeItEasy uses AdditionalAttributes. You guys expose
|
I don't mind the breaking change. It's not a very widely used feature, so it shouldn't affect many users, and the fix will be pretty easy anyway. @adamralph, @blairconrad, what do you think?
That's a good question, I hadn't thought about that. We already expose part of the DynamicProxy API as a result of using ILMerge, but not directly as part of the FakeItEasy API. I'd rather avoid that if possible, but I'm not sure how. I guess we could create yet another
I guess that would work too, and it would have the benefit of avoiding the breaking change entirely. Of course, as always when using reflection on private members, there's the risk of the internals being changed in a BCL update, but I don't think it's very likely. |
Slightly off-topic, but related: while working on this I noticed the obsolete |
I haven't looked at the changes in depth, but in general it looks good to me.
Well worth it, in my opinion. I'd go for the breaking change.
Yes! This. 😻 |
return false; | ||
} | ||
return true; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
returns false if x has members not in y, but not the other way 'round, no?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think most of the comparisons can be short-circuited by checking the length of all the collections before even checking any contents, that would fix this problem.
Yep, it was marked obsolete in 2009 so it can go, I actually wrote about it in my last comment then removed the mention to avoid making things more complicated for now. If it gets in your way then remove it, otherwise lets do it separate and update our changelog.
We can add a I think we should turn your PoC into a PR ready to merge. i.e. some unit tests, update the changelog and docs. |
Actually, let's remove |
Thanks for the feedback guys! I'll make the requested changes.
I'll add some tests, but I didn't want to waste time doing it until the idea was approved 😉 |
I was also thinking that we could do the same for FakeItEasy even before this PR makes it into a prime-time Castle.Core. It would mean duplicate work that would later be ripped out, but could provide a gentler introduction to the new way to specify attributes. Our clients could have a chance to convert before we make a breaking change. |
Up to you, however I plan to release 4.0.0-beta2 after we get this merged. Need to sort out #200, then plan to ship a final release very soon. |
5d034e6
to
fd5c39a
Compare
I realized my implementation was incomplete, as it didn't properly support arrays as attribute arguments (attributes support primitive types, I also removed I still need to add unit tests, but that's all for today. BTW I'm having a hard time working on the Castle Core code base. The non-.NET Core solution doesn't build in Visual Studio 2015 because of the project.json files. The .NET Core solution builds, but running the tests in the IDE doesn't work (tried with the VS runner and the R# runner). Does it work for you @jonorossi ? |
I can't find the .NET CLI issue right now but the updated VS2015 MSBuild targets files break the csproj projects when there is a project.json file in the same directory, so I've been using VS2013, I was actually using VS2013 until someone else reported the problem a while back. My suggestion either use VS2013 or temporarily delete project.json, it is all short lived so I'm not interested in hacking our csproj files to workaround the defect. I use ReSharper's test runner in VS2013 for the .NET Framework projects, and the VS test runner in VS2015 for the .NET Core project. |
Did we make a decision about the FIE API? I don't think we should expose a Castle.Core type in our public API. We don't currently, and effectively Castle.Core is a hidden implementation detail. We exclude the three types from internalisation when IL merging, but that's only due to the mechanics of Castle.Core. We don't expose those types in our API. |
Obviously that is for you guys to ultimately decide, but I liked @thomaslevesque suggestion and that was enough of a solution for me to proceed knowing you guys can continue to keep DynamicProxy an implementation detail:
|
@adamralph, I would also like to
|
Thanks for the suggestion. I no longer have VS2013, so I've been deleting the project.json and project.lock.json.
Not a formal decision, but I think we all agree about not exposing a Castle Core type in our API. My preference would be to just expose the expression-based API, i.e.:
|
private static readonly IEqualityComparer<object> ValueComparer = new AttributeArgumentValueEqualityComparer(); | ||
|
||
private readonly CustomAttributeBuilder builder; | ||
private readonly ConstructorInfo con; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A minor thing, but I don't love "con" as a name. Perhaps its well known, it did slow me down a little. Consider "constructor"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't love it either, but I chose it because it's the name used in the CustomAttributeBuilder
constructor, so it seemed the natural choice since CustomAttributeInfo
is meant to be a "clone" of that class. But I can change it to something else of course.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As I say, a minor thing. I'm content to go along.
var assignment = binding as MemberAssignment; | ||
if (assignment == null) | ||
{ | ||
throw new ArgumentException("Only assignments bindings are supported"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"assignment bindings"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oops. I'll fix it, thanks
return array; | ||
} | ||
} | ||
throw new ArgumentException("Only constant and single-dimensional array expressions are supported"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We may reach this line in a case where a single-dimensional array expression is not supported. (When allowArray
is false
.)
I'd consider reworking the flow to avoid a potentially misleading exception message.
While I'm commenting on it, if such a rework avoided a boolean parameter, I'd consider it an improvement. I never know what these mean. Way back on line 113, I was all '"true"? What's "true" mean?".
But it's not such a huge deal, given that it's a private method and we can find the answer pretty quickly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(I thought I had sent this comment, but apparently Github thought I had just started a review... weird)
We may reach this line in a case where a single-dimensional array expression is not supported. (When allowArray is false.)
I'd consider reworking the flow to avoid a potentially misleading exception message.
I thought about it, but in fact, it can only happen if we find an array inside an array. Attribute arguments support single-dimension arrays, but not nested arrays. So in the end the exception message would still be correct.
While I'm commenting on it, if such a rework avoided a boolean parameter, I'd consider it an improvement. I never know what these mean. Way back on line 113, I was all '"true"? What's "true" mean?".
In this case true
means that the value is allowed to be an array. The only case where it's false
is when called recursively examining array elements (to avoid nested arrays). I can rename it and/or use a named argument on the call site to make things clearer if necessary. I don't see an elegant way to avoid the bool
parameter and avoid awkward duplication...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it can only happen if we find an array inside an array
Ah! Yes. Thanks. I see it now after rereading. And having you tell me. 😊
I can rename it and/or use a named argument on the call site to make things clearer if necessary.
The meaning of the parameter is quite clear on the inside of the method. And I like the name.
For me, a named argument would improve the call sites. I know they're not everyone's cup of tea, though.
An alternative would be a microtype (probably an enum) so we could have explicitly named values at the call sites, but again, maybe that's too much.
In the end, it's a private method, and not hard for anyone who's confused by the calls to look up the meaning of the bool. Thanks for listening.
{ | ||
} | ||
|
||
public static CustomAttributeInfo FromExpression(Expression<Func<Attribute>> expression) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm sure this is perfectly correct, but it would be nice to see it exercised at least once in the tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It will. I'm not done yet ;)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm sorry. I must've missed that note. I thought you'd finished it up. What a nag you must think me. Of course now I see the [WIP]
, which has never left.
4dfda22
to
a788e16
Compare
OK, I think I'm done now. Added some tests fixed a few issues, and rebased onto master. |
FWIW, it looks very good to me. Thanks, @thomaslevesque! |
[Obsolete( | ||
"This property is obsolete and will be removed in future versions. Use AdditionalAttributes property instead. " + | ||
"You can use AttributeUtil class to simplify creating CustomAttributeBuilder instances for common cases.")] | ||
public IList<Attribute> AttributesToAddToGeneratedTypes |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you please update the changelog with a breaking change for two of these properties and removing IAttributeDisassembler
.
@@ -167,6 +167,7 @@ | |||
<Compile Include="Components.DictionaryAdapter.Tests\IPhoneWithFetch.cs" /> | |||
<Compile Include="Components.DictionaryAdapter.Tests\NameValueCollectionAdapterTests.cs" /> | |||
<Compile Include="DynamicProxy.Tests\AttributesToAvoidReplicatingTestCase.cs" /> | |||
<Compile Include="CustomAttributeInfoTestCase.cs" /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you move these unit tests into the DynamicProxy.Tests directory.
} | ||
} | ||
} | ||
; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a stray semicolon at the end here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops, don't know how it ended up here...
this.constructorArgs = constructorArgs; | ||
|
||
this.properties = namedProperties | ||
.Zip(propertyValues, (p, v) => new { p.Name, Value = v }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Enumerable.Zip
isn't available on .NET 3.5, it was added in .NET 4.0.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Argh, forgot about that... I guess I'll just use a for
loop. I could also add a compatibility shim, but that would probably be overkill for that use case.
this.builder = new CustomAttributeBuilder(constructor, constructorArgs, namedProperties, propertyValues, namedFields, fieldValues); | ||
|
||
this.constructor = constructor; | ||
this.constructorArgs = constructorArgs; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we copy the constructorArgs array to make sure this type stays immutable?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. Should probably do it for the other parameters too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should probably do it for the other parameters too.
Scratch that. The other parameters are used to build dictionaries, not kept as is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That was my thought at first as well. Then I had a second thought. It'll probably just reveal my inadequate grasp of what's going on here, but may not individual elements of constructorArgs
or the other *Values parameters be single-dimensional arrays, which could be mutated after the fact?
15e123a
to
8d07b6c
Compare
@jonorossi I made the requested changes |
btw @jonorossi let me know if you want me to squash some or all the commits before the merge. I don't know how you usually handle this in Castle.Core |
It doesn't matter for However you have a point about individual args of |
Which is a way to store them, no? In dictionaries! dict.Add(members[i].Name, values[i]); shoves the element of the I do agree that anyone changing the hypothetical arrays after the fact is looking for trouble. Then again, when they find it, they'll probably come back with questions and complaints! 😉 |
Ah, I thought you were talking about
True. @jonorossi, what do you think? Should the code account for that by deep-cloning the arrays? (actually, only nested arrays need to be cloned, since all other types allowed as attribute arguments are immutable) |
To avoid wasted allocations I think we should leave the code as is, and just add an XML comment to the class indicating that any arrays passed to the class are now owned by that class and should not be modified. I agree this is an edge case and I'm happy to direct defect reports to an XML comment. |
I do usually like history in PRs squished if they aren't commits that align to nice clean patches (I'm old school, I like clean patch sets 😉 ), but have got soft over the years as just getting a complete PR seems to be a stretch lately. Many thanks for the work here and being so responsive. |
8d07b6c
to
e67a530
Compare
I don't mind squashing into clean commits, that's what we usually do in FakeItEasy. I squashed the changes down to 4 commits. |
@thomaslevesque just wanted to make sure you didn't miss my reply to your question. Were you going to add that XML comment? |
Actually, I did miss it... I'll add the comment. |
I added the XML comment in a fixup commit; I'll squash it after you approve it. |
Exactly what I was expecting, just without the spelling mistake 😉 :
|
to implement proper equality comparison
and related code (AttributeDisassembler)
362f099
to
cae3f9f
Compare
Oops! Fixed and squashed |
Many thanks @thomaslevesque, I'm ready to merge. |
Thanks, @thomaslevesque and @jonorossi! |
Thanks for the merge! |
* Fix compilation error > MockRepository.cs(213, 17): [CS0618] 'ProxyGenerationOptions.AttributesToAddToGeneratedTypes' is obsolete: 'This property is obsolete and will be removed in future versions. Use AdditionalAttributes property instead. You can use AttributeUtil class to simplify creating CustomAttributeBuilder instances for common cases.' See details: * https://github.com/castleproject/Core/blob/master/CHANGELOG.md#400-beta002-2016-10-28 * castleproject/Core#219
Fixes #77
At this point, this PR is mostly a proof of concept; I didn't try to preserve the existing API for backwards compatibility, I just changed the type of
AdditionalAttributes
fromCustomAttributeBuilder
to a newCustomAttributeInfo
class. This class has the same constructors asCustomAttributeBuilder
, and overridesEquals
andGetHashCode
. I also introduce aCustomAttributeInfo.FromExpression
method to make it easier to define attributes: