-
Notifications
You must be signed in to change notification settings - Fork 90
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
BREAKING: Fix InvalidOperationException
when using PropertiesDictionary in a multithreaded application and remove [Serializable] for it.
#2415
Changes from 3 commits
bef3fbe
7377912
725f201
d43c31e
8eb304d
bc850ac
649f09e
74d2451
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,6 +3,7 @@ | |
|
||
using System; | ||
using System.Collections; | ||
using System.Collections.Concurrent; | ||
using System.Collections.Generic; | ||
using System.Globalization; | ||
using System.IO; | ||
|
@@ -585,5 +586,15 @@ internal static void ConsumeElementOfDepth(this XmlReader xmlReader, int endElem | |
xmlReader.Read(); | ||
} | ||
} | ||
|
||
public static bool Add<T>(this ConcurrentDictionary<string, T> concurrentDictionary, string key, T value) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why does this extension method return a bool? IDictionary.Add returns void. It's not a good idea to create a strange return value for 'Add', this will confuse experienced .NET programmers. If you need to override 'Add', so that a concurrent dictionary behaves properly when it flows around to IDictionary-accepting things, this should return void. And in the case where Add would throw, you should consult the return value of TryAdd and throw on false. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. agree and updated, see the other comment. |
||
{ | ||
return concurrentDictionary.TryAdd(key, value); | ||
} | ||
|
||
public static bool Remove<T>(this ConcurrentDictionary<string, T> concurrentDictionary, string key) | ||
{ | ||
return concurrentDictionary.TryRemove(key, out _); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,11 +2,11 @@ | |
// Licensed under the MIT license. See LICENSE file in the project root for full license information. | ||
|
||
using System; | ||
using System.Collections.Concurrent; | ||
using System.Collections.Generic; | ||
using System.Collections.Immutable; | ||
using System.ComponentModel; | ||
using System.IO; | ||
using System.Runtime.Serialization; | ||
using System.Xml; | ||
|
||
using Newtonsoft.Json; | ||
|
@@ -37,11 +37,6 @@ public PropertiesDictionary( | |
{ | ||
} | ||
|
||
protected PropertiesDictionary(SerializationInfo info, StreamingContext context) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the .net core concurrentdictionary is not binary serializable, so I removed it, question, do we even need it, it is not for json and xml serialization, it is only for BinaryFormatter serialization, in the solution I don't see code use BinaryFormatter There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's a good question and in future, definitely try to raise this before making this kind of change. Remember we are in an SDK, so it is possible that someone is using BinaryFormatter with this public type (even though we do not as you point out). I think probably it is unlikely we're breaking someone out there. So this change is fine. However we need to very clearly mark this breaking change in behavior in the release notes. Imagine someone using BInaryFormatter with this type, using an older SDK. Now they update to the new SDK and everything is broken. Ideally, your release notes would also mention the symptom these users would receive. Make sense? IOW, try to use BinaryFormatter again PropertiesDictionary (once you remove the [Serializable] attribute as well. See what exception is raised. Make sure the release notes reference this exception in case users experience it. By doing this, we leave an easily followed trail of clues that tell developers what happened. That's the least we can do, given that we're introducing a breaking change. :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. thanks for the great inputs, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. additional question, do you want me to create a work item to check and remove all [Serializable] in the solution, https://docs.microsoft.com/en-us/dotnet/standard/serialization/binaryformatter-security-guide https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca2300 |
||
: base(info, context) | ||
{ | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. check if the serialization will be the same comparing previous vs current #Closed There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. got it, thanks There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the file written are the same |
||
|
||
public string Name { get; set; } | ||
|
||
public virtual T GetProperty<T>(PerLanguageOption<T> setting) | ||
|
@@ -91,7 +86,7 @@ public void SetProperty(IOption setting, object value, bool cacheDescription, bo | |
|
||
if (cacheDescription) | ||
{ | ||
SettingNameToDescriptionsMap = SettingNameToDescriptionsMap ?? new Dictionary<string, string>(); | ||
SettingNameToDescriptionsMap ??= new ConcurrentDictionary<string, string>(); | ||
SettingNameToDescriptionsMap[setting.Name] = setting.Description; | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,6 +3,7 @@ | |
|
||
using System; | ||
using System.Collections; | ||
using System.Collections.Concurrent; | ||
using System.Collections.Generic; | ||
using System.Collections.Specialized; | ||
using System.ComponentModel; | ||
|
@@ -27,7 +28,7 @@ public static void SavePropertiesToXmlStream( | |
XmlWriter writer, | ||
XmlWriterSettings settings, | ||
string name, | ||
Dictionary<string, string> settingNameToDescriptionMap) | ||
ConcurrentDictionary<string, string> settingNameToDescriptionMap) | ||
{ | ||
if (propertyBag == null) | ||
{ | ||
|
@@ -73,33 +74,28 @@ public static void SavePropertiesToXmlStream( | |
StringSet stringSet = property as StringSet; | ||
if (stringSet != null) | ||
{ | ||
SaveSettingNameToDescriptionMap(writer, key, settingNameToDescriptionMap); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. found this new method is not needed, removed. |
||
SaveSet(writer, stringSet, key); | ||
continue; | ||
} | ||
|
||
IntegerSet integerSet = property as IntegerSet; | ||
if (integerSet != null) | ||
{ | ||
SaveSettingNameToDescriptionMap(writer, key, settingNameToDescriptionMap); | ||
SaveSet(writer, integerSet, key); | ||
continue; | ||
} | ||
|
||
IDictionary pb = property as IDictionary; | ||
if (pb != null) | ||
{ | ||
SaveSettingNameToDescriptionMap(writer, key, settingNameToDescriptionMap); | ||
((IDictionary)pb).SavePropertiesToXmlStream(writer, settings, key, settingNameToDescriptionMap); | ||
continue; | ||
} | ||
|
||
if (settingNameToDescriptionMap != null) | ||
{ | ||
string description; | ||
|
||
if (settingNameToDescriptionMap.TryGetValue(key, out description)) | ||
{ | ||
writer.WriteComment(description); | ||
} | ||
} | ||
SaveSettingNameToDescriptionMap(writer, key, settingNameToDescriptionMap); | ||
|
||
writer.WriteStartElement(PROPERTY_ID); | ||
writer.WriteAttributeString(KEY_ID, key); | ||
|
@@ -119,6 +115,19 @@ public static void SavePropertiesToXmlStream( | |
writer.WriteEndElement(); // Properties | ||
} | ||
|
||
private static void SaveSettingNameToDescriptionMap(XmlWriter writer, string key, ConcurrentDictionary<string, string> settingNameToDescriptionMap) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What are we doing here? Seems like you found a different issue and are oportunistically fixing it? [edit] oh, yes, I see the bug, because we 'continue', we didn't call this code. Why didn't you just move the logical earlier in the method? And avoid injecting four calls to your new method? Is that possible? #Resolved There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. there were no tests for this and when I add tests this issue show up and blocking my test pass so I should fix it together. avoid injecting four calls to your new method ---- agree, let me take a look There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fixed to only one copy. the method is not needed. |
||
{ | ||
if (settingNameToDescriptionMap != null) | ||
{ | ||
string description; | ||
|
||
if (settingNameToDescriptionMap.TryGetValue(key, out description)) | ||
{ | ||
writer.WriteComment(description); | ||
} | ||
} | ||
} | ||
|
||
private static string NormalizeTypeName(string typeName) | ||
{ | ||
// This helper currently assumes that namespaces in | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,8 +2,8 @@ | |
// Licensed under the MIT license. See LICENSE file in the project root for full license information. | ||
|
||
using System; | ||
using System.Collections.Concurrent; | ||
using System.Collections.Generic; | ||
using System.Runtime.Serialization; | ||
|
||
using Newtonsoft.Json; | ||
|
||
|
@@ -13,7 +13,7 @@ public interface IMarker { } | |
|
||
[Serializable] | ||
[JsonConverter(typeof(TypedPropertiesDictionaryConverter))] | ||
public class TypedPropertiesDictionary<T> : Dictionary<string, T>, IMarker where T : new() | ||
public class TypedPropertiesDictionary<T> : ConcurrentDictionary<string, T>, IMarker where T : new() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Update the title of the PR and add a new entry to the ReleaseHistory. The title should be something like: "Fix InvalidOperationException when using PropertiesDictionary in a multithreaded application". With that, if any customer checks the release history, the customer would know what motivated us to change this from Dictionary to ConcurrentDictionary. This should also be applied to the PR description. #Resolved There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done both, thanks There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. also added the ReleaseHistory for the other 2 PRs. |
||
{ | ||
public TypedPropertiesDictionary() : this(null, StringComparer.Ordinal) | ||
{ | ||
|
@@ -30,13 +30,8 @@ public TypedPropertiesDictionary(PropertiesDictionary initializer, IEqualityComp | |
} | ||
} | ||
|
||
protected TypedPropertiesDictionary(SerializationInfo info, StreamingContext context) | ||
: base(info, context) | ||
{ | ||
} | ||
|
||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")] | ||
protected Dictionary<string, string> SettingNameToDescriptionsMap { get; set; } | ||
protected ConcurrentDictionary<string, string> SettingNameToDescriptionsMap { get; set; } | ||
|
||
public virtual T GetProperty(PerLanguageOption<T> setting) | ||
{ | ||
|
@@ -74,7 +69,7 @@ public virtual void SetProperty(IOption setting, T value, bool cacheDescription) | |
|
||
if (cacheDescription) | ||
{ | ||
SettingNameToDescriptionsMap = SettingNameToDescriptionsMap ?? new Dictionary<string, string>(); | ||
SettingNameToDescriptionsMap ??= new ConcurrentDictionary<string, string>(); | ||
SettingNameToDescriptionsMap[setting.Name] = setting.Description; | ||
} | ||
|
||
|
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.
Why did you add this? What does the CurrentDictionary IDictionary.Add implementation call into? A non-thread-safe base method? #Resolved
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.
just checked the source code of CurrentDictionary,
the IDictionary<TKey, TValue>.Add implementation
did call the same method as the TryAdd
so we can use it thread safely.
Also agree that we should not make the Add has a return value people will get confused.
updated the code.