In System.Text.Json how does the "setter" work when using properties that are of type List? it is not how i would expect, especially in relation to the setter for a string as example.
Here's what I mean:
public class Program
{
public static void Main()
{
var json = #"
{
""doot"": ""bloop bloop"",
""bars"":
[
{
""id"": 1
},
{
""id"": 2
}
]
}
";
var obj = JsonSerializer.Deserialize<Foo>(json);
Console.WriteLine($"obj.Doot: {obj.Doot}");
if (obj.Bars == null)
Console.WriteLine("Bars is null.");
else
Console.WriteLine($"obj.Bars.Count: {obj.Bars.Count}");
}
public class Foo
{
private string _doot;
[JsonPropertyName("doot")]
public string Doot
{
get => _doot;
set
{
Console.WriteLine($"Setting Doot to {value}");
_doot = value;
}
}
private List<Bar> _bars;
[JsonPropertyName("bars")]
public List<Bar> Bars
{
get => _bars;
set
{
Console.WriteLine($"Bars Value count during setter: {value.Count}");
_bars = value;
}
}
}
public class Bar
{
[JsonPropertyName("id")]
public int Id {get;set;}
}
}
output:
Setting Doot to bloop bloop
Bars Value count during setter: 0
obj.Doot: bloop bloop
obj.Bars.Count: 2
In the above, when the Doot setter fires, value has the value being set to the backing field _doot. When the Bars setter fires, value has something in it, but it is not the two Bar instances, as shown by the Console.WriteLine statement within the setter, which reports a 0 count.
If I needed to do some logic based on the values being set within the property Bars, how can I accomplish that if value does not have the information?
fiddle for reference:
https://dotnetfiddle.net/7z2O9S
EDIT:
it was pointed out to me elsewhere (caschw if you're here thanks) that the list is likely initialized as an empty list, then list.Add or list.AddRange could be used after initialization (i'm unclear which is used under the covers of System.Text.Json); meaning the setter would only ever be done once unless I change the reference to the list. This makes sense, but still, how would I hook into running some logic whenever the collection of Bars is changed?
The assumption in your edit is correct. The way the list is deserialized is that first the list object is being created, and then the individual elements are added. If you think about how JSON works, then it makes sense for a parser and deserializer to work that way:
When the parser encounters a [ it knows that a JSON array is being started. So when that object is to be deserialized into a .NET collection type, then it can already create that collection at that moment. After all, the deserializer knows the target type it should deserialize to.
So the deserializer creates the list object, and then starts to deserialize the items inside. In your case, these are Bar objects. So it constructs those, sets the properties, and—once the object is completed—finally adds them to the list.
You can actually see this in the source. There are a number of collection converters, depending on what target type you have, but they all inherit from IEnumerableDefaultConverter which basically has the behavior I described above. For List<T>, the actual work then happens in the ListOfTConverter which basically just initializes a new list and calls Add for each item.
If I needed to do some logic based on the values being set within the property Bars, how can I accomplish that if value does not have the information?
I don’t think you should. You should see the deserialization process as a single operation and not interfere with it. I would suggest you to do that afterwards, e.g. as a post-processing step after deserialization.
Related
I'm only using Code Analysis for cleaning, organizing and ensuring these changes are globally performed for all instances of a particular warning. I'm down to the final, and it's CA2227.
CA2227 Collection properties should be read only Change '' to be
read-only by removing the property setter.
Note this is for mapping of EDI documents. These classes are to represent a whole or part of an EDI document.
public class PO1Loop
{
public SegmentTypes.PO1LoopSegmentTypes.PO1 PO1 { get; set; }
public Collection<SegmentTypes.PO1LoopSegmentTypes.PID1> PIDRepeat1 { get; set; }
public Collection<SegmentTypes.PO1LoopSegmentTypes.PID2> PIDRepeat2 { get; set; }
public SegmentTypes.PO1LoopSegmentTypes.PO4 PO4 { get; set; }
/* Max Use: 8 */
public Collection<SegmentTypes.PO1LoopSegmentTypes.ACK> ACKRepeat { get; set; }
}
You can see all of the Collection properties will give me this warning, and there are hundreds of them. When using the above class I instantiate it without any data. Then externally I add the data and set each individual variable through its public accessor. I do not instantiate this class with all the data prepared and passed using a constructor method (IMO for the size these can reach it can easily wreak havoc on the eyes). When complete and all properties are assigned the class as a whole is then used to generate that part of a document it represents.
My question is, for the usage described above, what would be a better approach for setting this up correctly? Do I keep the public accessors and suppress this warning entirely, or is there a entirely different solution that would work?
Here's what MSDN says about the error, and also how you can avoid it.
Here's my take on the issue.
Consider, the following class:
class BigDataClass
{
public List<string> Data { get; set; }
}
This class will throw that exact same issue. Why? Because Collections do not need a setter. Now, we can do anything with that object: assign Data to an arbitrary List<string>, add elements to Data, remove elements from Data, etc. If we remove the setter, we only lose the ability to directly assign to that property.
Consider the following code:
class BigDataClass
{
private List<string> data = new List<string>();
public List<string> Data { get { return data; } } // note, we removed the setter
}
var bigData = new BigDataClass();
bigData.Data.Add("Some String");
This code is perfectly valid and in fact the recommended way to do things. Why? Because the List<string> is a reference to a memory location, that contains the remainder of the data.
Now, the only thing you cannot now do with this, is directly set the Data property. I.e. the following is invalid:
var bigData = new BigDataClass();
bigData.Data = new List<string>();
This is not necessarily a bad thing. You'll notice that on many .NET types this model is used. It's the basics of immutability. You usually do not want direct access to the mutability of Collections, as this can cause some accidental behavior that has strange issues. This is why Microsoft recommends you omit setters.
Example:
var bigData = new BigDataClass();
bigData.Data.Add("Some String");
var l2 = new List<string>();
l2.Add("String 1");
l2.Add("String 2");
bigData.Data = l2;
Console.WriteLine(bigData.Data[0]);
We might be expecting Some String, but we'll get String 1. This also means that you cannot reliably attach events to the Collection in question, so you cannot reliably determine if new values are added or values are removed.
A writable collection property allows a user to replace the collection with a completely different collection.
Essentially, if you only ever need to run the constructor, or assignment, once, then omit the set modifier. You won't need it, direct assignment of collections is against best-practices.
Now, I'm not saying never use a setter on a Collection, sometimes you may need one, but in general you should not use them.
You can always use .AddRange, .Clone, etc. on the Collections, you only lose the ability of direct assignment.
Serialization
Lastly, what do we do if we wish to Serialize or Deserialize a class that contains our Collection without a set? Well, there is always more than one way to do it, the simplest (in my opinion) is to create a property that represents the serialized collection.
Take our BigDataClass for example. If we wished to Serialize, and then Deserialize this class with the following code, the Data property would have no elements.
JavaScriptSerializer jss = new JavaScriptSerializer();
BigDataClass bdc = new BigDataClass();
bdc.Data.Add("Test String");
string serd = jss.Serialize(bdc);
Console.WriteLine(serd);
BigDataClass bdc2 = jss.Deserialize<BigDataClass>(serd);
So, to fix this, we can simply modify our BigDataClass a bit to make it use a new string property for Serialization purposes.
public class BigDataClass
{
private List<string> data = new List<string>();
[ScriptIgnore]
public List<string> Data { get { return data; } } // note, we removed the setter
public string SerializedData { get { JavaScriptSerializer jss = new JavaScriptSerializer(); return jss.Serialize(data); } set { JavaScriptSerializer jss = new JavaScriptSerializer(); data = jss.Deserialize<List<string>>(value); } }
}
Another option is always the DataContractSerializer (which is really a better option, in general.) You can find information about it on this StackOverflow question.
With current VS2019 we can simply do this:
public List<string> Data { get; } = new List<string>();
This satisfies CA2227 and can be serialized/deserialized.
The deserialization works because List<> has an "Add" method, and the serializer knows how to handle a read-only collection property with an Add method (the property is read-only but not the elements) (I use Json.Net, other serializers may behave differently).
Edit:
As pointed out it should be "=" and not "=>" (compiler will prevent you using "=>"). If we used "public List Data => new List();" then it would create a new list every time the property was accessed which is not what we want either.
Edit:
Note that this will NOT work if the type of the property is an interface, such as IList
Edit:
I think the handling of interfaces is determined by the serializer used. The following works perfectly. I'm sure all common serializers know how to handle ICollection. And if you have some custom interface that does not implement ICollection then you should be able to configure the serializer to handle it, but in that case CA2227 probably won't be triggered making it irrelevant here. (As it is a read-only property you have to assign a concrete value within the class so it should always be serializing and de-serializing a non-null value)
public class CA2227TestClass
{
public IList Data { get; } = new List<string>();
}
[TestMethod]
public void CA2227_Serialization()
{
var test = new CA2227TestClass()
{
Data = { "One", "Two", "Three" }
};
var json = JsonConvert.SerializeObject(test);
Assert.AreEqual("{\"Data\":[\"One\",\"Two\",\"Three\"]}", json);
var jsonObject = JsonConvert.DeserializeObject(json, typeof(CA2227TestClass)) as CA2227TestClass;
Assert.IsNotNull(jsonObject);
Assert.AreEqual(3, jsonObject.Data.Count);
Assert.AreEqual("One", jsonObject.Data[0]);
Assert.AreEqual("Two", jsonObject.Data[1]);
Assert.AreEqual("Three", jsonObject.Data[2]);
Assert.AreEqual(typeof(List<string>), jsonObject.Data.GetType());
}
💡 Alternative Solution 💡
In my situation, making the property read-only was not viable because the whole list (as a reference) could change to a new list.
I was able to resolve this warning by changing the properties' setter scope to be internal.
public List<Batch> Batches
{
get { return _Batches; }
internal set { _Batches = value; OnPropertyChanged(nameof(Batches)); }
}
Note one could also use private set...
The hint's (achilleas heal) of this warning seems really pointed to libraries for the documentation says (Bolding mine):
An externally visible writable property is a type that implements
System.Collections.ICollection.
For me it was, "Ok, I won't make it viewable externally...." and internal was fine for the app.
Thanks to #Matthew, #CraigW and #EBrown for helping me understanding the solution for this warning.
public class PO1Loop
{
public SegmentTypes.PO1LoopSegmentTypes.PO1 PO1 { get; set; }
public Collection<SegmentTypes.PO1LoopSegmentTypes.PID1> PIDRepeat1 { get; private set; }
public Collection<SegmentTypes.PO1LoopSegmentTypes.PID2> PIDRepeat2 { get; private set; }
public SegmentTypes.PO1LoopSegmentTypes.PO4 PO4 { get; set; }
/* Max Use: 8 */
public Collection<SegmentTypes.PO1LoopSegmentTypes.ACK> ACKRepeat { get; private set; }
public PO1Loop()
{
PIDRepeat1 = new Collection<SegmentTypes.PO1LoopSegmentTypes.PID1>();
PIDRepeat2 = new Collection<SegmentTypes.PO1LoopSegmentTypes.PID2>();
ACKRepeat = new Collection<SegmentTypes.PO1LoopSegmentTypes.ACK>();
}
}
When wanting to assign data to the collection types use AddRange, Clear or any other variation of method for modifying a collection.
Only while binding DTO, you need to suppress warnings.
otherwise a custom ModelBinder is required custom ModelBinder to bind collections.
quoting the rule documentation:
When to suppress warnings
You can suppress the warning if the property is part of a Data Transfer Object (DTO) class.
Otherwise, do not suppress warnings from this rule.
https://learn.microsoft.com/pt-br/visualstudio/code-quality/ca2227?view=vs-2019
DTOs often require serialization and deserialization. Thus, they are required to be mutable.
Having to create an alternate backing property is a pain.
Simply change the property type from List<string> to IReadOnlyList<string> then this works as expected without CA2227.
The collection is set via the property but you can also cast to List<string> if you wish to append or delete items.
class Holder
{
public IReadOnlyList<string> Col { get; set; } = new List<string>();
}
var list = new List<string> { "One", "Two" };
var holder = new Holder() { Col = list } ;
var json = JsonConvert.SerializeObject(holder);
// output json {"Col":["One","Two"]}
var deserializedHolder = JsonConvert.DeserializeObject<Holder>(json);
I had to fix some of the CA2227 violations, so i had to add the "readonly" keyword to the collection field and then of course, had to remove the setter property. Some code that have used the setter, just created a new collection object which initially was empty. This code sure did not compile so i had to add a SetXxx() method in order to realize the missing setter's functionality. I did it like this:
public void SetXxx(List<string> list)
{
this.theList.Clear();
this.theList.AddRange(list);
}
The code of callers using the setter has been replaced with a call to the method SetXxx().
Instead of creating a complete new list, the existing list now will be cleared and filled with new items from another list, passed in as a parameter. The original list, due to the fact it is readonly and created only once, will always remain.
I believe this is also a good way to avoid that the garbagae collector has to delete old objects that got out of scope and second, to create new collection objects although there is already one.
As an addition to Der Kommissar's excellent answer.
Starting with .NET 5 (C# 9.0) there are init-only properties. These properties are only settable under specific circumstances, see here for reference.
The following example should not raise a warning CA2227, yet still allow for the collection being set during object initialization.
using System.Collections.Generic;
namespace BookStore
{
public class BookModel
{
public ICollection<string> Chapters { get; init; }
}
}
Note that the current version of the .NET SDK still raises a warning when using the built-in analyzer (not the NuGet package). This is a known bug and should be fixed in the future.
To cover all the possible scenarios to resolve CA2227 error:
This covers the Entity relationship mapping when we use Entity Framework.
class Program
{
static void Main(string[] args)
{
ParentClass obj = new ParentClass();
obj.ChildDetails.Clear();
obj.ChildDetails.AddRange();
obj.LstNames.Clear();
obj.LstNames.AddRange();
}
}
public class ChildClass
{ }
public class ParentClass
{
private readonly ICollection<ChildClass> _ChildClass;
public ParentClass()
{
_ChildClass = new HashSet<ChildClass>();
}
public virtual ICollection<ChildClass> ChildDetails => _ChildClass;
public IList<string> LstNames => new List<string>();
}
Maybe it's just a misunderstanding but this is a big question for me. Let me explain it:
According to reference a property is a mechanism and not a field. A mechanism that provide read and write functions for a field, and according to this we can create a read-only, write-only or read-write property by using get and set accessors.
Now the implementation is here:
public class Foo
{
private List<string> _bar;
public List<string> Bar
{
get
{
return _bar;
}
}
public Foo()
{
_bar = new List<string>();
_bar.Add("string1");
}
}
In Foo class we have a read-only property (Bar) that consists of one string.
Now lets add a driver for this class:
static void Main(string[] args)
{
Foo fooObj = new Foo();
fooObj.Bar.Add("string2");
foreach (string s in fooObj.Bar)
{
Console.WriteLine(s);
}
Console.ReadLine();
}
And here is the big question mark:
Why the Bar property is not read-only?
output:
srring1
string2
I know how to create a read-only collection (my question is not why List<T> is not read-only) and I need a explanation about read-only properties.
Well the Bar property is read only i.e. it can't be directly set
fooObj.Bar = new List<string>(); // compiler error
However, the data returned by that property isn't
fooObj.Bar.Add("..."); // is fine
The point to understand is the modifier on a property dictates how it can be accessed from the object, it has no direct affect on the underlying data of the property. Therefore, returning a reference type from a read-only property is the exact same as returning a reference type from a read/write property.
In your example, if you wanted the Bar to be read-only then you could return a ReadOnlyCollection<T> which is an immutable collection, rather than a List<T>.
private List<string> _bar;
...
public void Add(string item)
{
_bar.Add(item);
}
public IEnumerable<string> Bar
{
get { return new ReadOnlyCollection<string>(_bar); }
}
This would keep control of the list with the containing object but allow you to return a readonly copy of the list itself.
Okay,
A List<T> is a reference type.
So, a property
List<string> SomeList
{
get
{
// ...
}
}
is a read-only property, you cannot set the SomeList to a different List<string>. Essentially,
SomeList = new List<string>();
will not compile.
As you note, making a property of a reference type read only does not make that type read only.
If you want a read only list I'd suggest,
IReadOnlyList<string> SomeList
{
get
{
// ...
}
}
Because the getter method of your Bar property returns the list, and then you are mutating that list.Omitting the setter method only prevents you to assign a new list directly like this:
fooObj.Bar = new List<string>();
If you want to make it immutable you can change return type of your property to IEnumerable<string> instead.. Though you can still cast it to list and then mutate...
The Bar property is read-only. You can only read the list Bar points to - you cannot make Bar point to some other list.
However, the list itself is mutable. You can add or remove items from it.
Eric lippert calls this "shallow immutability" in his blog post Immutability in C# Part One: Kinds of Immutability. The property is read-only, but its contents can change.
I have a ListBox, and it's items consist of custom class objects (can be any class).
Then I set the DisplayMemberPath so the ListBox shows the right property of that custom class fine.
Now I need to enumerate the Items list of ListBox, and get the DisplayMember value of each item in the list, without knowing the type of the class in the list. Is there any way to get this DisplayMember value without Reflection?
In WPF, you don't need to implement an interface, or a base class for a container control to read the value of a property. In an ideal world, it would make sense to declare a base class or interface and have all of your custom classes extend, or implement these, but the benefit of that is really to keep your data type safe.
For example, in WPF, this is perfectly legal and will work just the same:
public class RadioButtonData
{
public string Label { get; set; }
public bool IsSelected { get; set; }
}
public class CustomData
{
public string Label { get; set; }
public string Value { get; set; }
}
...
private ObservableCollection<object> objects = new ObservableCollection<object>();
public ObservableCollection<object> Objects
{
get { return objects; }
set { objects = value; NotifyPropertyChanged("Objects"); }
}
...
Objects.Add(new RadioButtonData() { Label = "Some Value" });
Objects.Add(new CustomData() { Label = "Another Value" });
...
<ListBox ItemsSource="{Binding Objects}" DisplayMemberPath="Label" />
So as long as your various classes have the same name of property, then they will all be displayed in the same way, like above. They don't even have to be of the same type... just as long as the name matches that used in the ListBox.DisplayMemberPath property.
UPDATE >>>
Ah sorry, I misunderstood your question. In the case that you want to access these property values in code, then you have four basic options:
Define an Interface with a particular property and make your custom classes implement it.
Declare a base class with a particular property and make your custom classes extend it.
Create a (potentially long) section of if else statements that checks the type of each object and then accesses the relevant property.
Use reflection.
In my personal opinion, I would recommend options 1 or 2 first, then 4 and lastly 3. I'm really not sure what you have against reflection, but it's really not that bad, or slow... I'd certainly prefer to use it rather than having an else if statement for every possible type used.
I have a class with an indexer property, with a string key:
public class IndexerProvider {
public object this[string key] {
get
{
return ...
}
set
{
...
}
}
...
}
I bind to an instance of this class in WPF, using indexer notation:
<TextBox Text="{Binding [IndexerKeyThingy]}">
That works fine, but I want to raise a PropertyChanged event when one of the indexer values changes. I tried raising it with a property name of "[keyname]" (i.e. including [] around the name of the key), but that doesn't seem to work. I don't get binding errors in my output window whatsoever.
I can't use CollectionChangedEvent, because the index is not integer based. And technically, the object isn't a collection anyway.
Can I do this, and so, how?
According to this blog entry, you have to use "Item[]". Item being the name of the property generated by the compiler when using an indexer.
If you want to be explicit, you can decorate the indexer property with an IndexerName attribute.
That would make the code look like:
public class IndexerProvider : INotifyPropertyChanged {
[IndexerName ("Item")]
public object this [string key] {
get {
return ...;
}
set {
... = value;
FirePropertyChanged ("Item[]");
}
}
}
At least it makes the intent more clear. I don't suggest you change the indexer name though, if your buddy found the string "Item[]" hard coded, it probably means that WPF would not be able to deal with a different indexer name.
Additionaly, you can use
FirePropertyChanged ("Item[IndexerKeyThingy]");
To notify only controls bound to IndexerKeyThingy on your indexer.
There are at least a couple of additional caveats when dealing with INotifyPropertyChang(ed/ing) and indexers.
The first is that most of the popular methods of avoiding magic property name strings are ineffective. The string created by the [CallerMemberName] attribute is missing the '[]' at the end, and lambda member expressions have problems expressing the concept at all.
() => this[] //Is invalid
() => this[i] //Is a method call expression on get_Item(TIndex i)
() => this //Is a constant expression on the base object
Several other posts have used Binding.IndexerName to avoid the string literal "Item[]", which is reasonable, but raises the second potential issue. An investigation of the dissasembly of related parts of WPF turned up the following segment in PropertyPath.ResolvePathParts.
if (this._arySVI[i].type == SourceValueType.Indexer)
{
IndexerParameterInfo[] array = this.ResolveIndexerParams(this._arySVI[i].paramList, obj, throwOnError);
this._earlyBoundPathParts[i] = array;
this._arySVI[i].propertyName = "Item[]";
}
The repeated use of "Item[]" as a constant value suggests that WPF is expecting that to be the name passed in the PropertyChanged event, and, even if it doesn't care what the actual property is called (which I didn't determine to my satisfaction one way or the other), avoiding use of [IndexerName] would maintain consistency.
Actually, I believe setting the IndexerName attribute to "Item" is redundant. The IndexerName attribute is specifically designed to rename an index, if you want to give it's collection item a different name. So your code could look something like this:
public class IndexerProvider : INotifyPropertyChanged {
[IndexerName("myIndexItem")]
public object this [string key] {
get {
return ...;
}
set {
... = value;
FirePropertyChanged ("myIndexItem[]");
}
}
}
Once you set the indexer name to whatever you want, you can then use it in the FirePropertyChanged event.
This is one i struggled with for ages so thought I'd document somewhere. (Apologies for asking and answering a question.)
(C# .net 2.0)
I had a class that was being serialized by XmlSerializer, I added a new public property however it wasn't being included in the output XML.
It's not mentioned in the docs anywhere I could find, but public properties must have a set as well as a get to be serialized! I guess this is because it assumes that if you're going to serialize then you'll want to deserialize from the same file, so only serializes properties that have both a set and a get.
As mentioned, most properties must have both a getter and setter; the main exception to this is lists - for example:
private readonly List<Foo> bar = new List<Foo>();
public List<Foo> Bar {get { return bar; } } // works fine
which will work fine; however, if XmlSerializer finds a setter - it demands that it is public; the following will not work:
public List<Foo> Bar {get; private set;} // FAIL
Other reasons it might not serialize:
it isn't public with get and set (or is readonly for a field)
it has a [DefaultValue] attribute, and is with that value
it has a public bool ShouldSerializeFoo() method that returned false
it has a public bool FooSpecified {get;set;} property or field that returned false
it is marked [XmlIgnore]
it is marked [Obsolete]
Any of these will cause it not to serialize
The point about getter+setter is made in the 3rd paragraph on the "Intro to Xml Serialization" page. It's actually in a call-out box. Can't miss it!
Intro-to-XML Serialization http://www.freeimagehosting.net/uploads/2f04fea2db.png
(having a little too much fun with Freeimagehosting.net)
Also properties that return null are not serialized!
if you don't want to implement proper Setters (because maybe you are neither wanting to deserialize or change an objects value) you can just use dummy setters like this set { }, so that the XMLSerializer works, but nothing happens if you use the Setter...
i.E.
public string ID { get { return _item.ID.ToString(); } set { } }
And if your class inherits a list and also has its own members, only the elements of the list get serialized. The data present in your class members is not captured.
Took some time figuring out this!
One more thing to add about serialization of collections:
The XmlSerializer ignores collections of interfaces!
And by that I mean ignore. While you will get an exception for a line like:
public IFoo Foo { get; set; }
you will not get an exception for:
public ICollection<IFoo> LotsOfFoos { get { return this.fooBackingField; } }
You can implement the IXmlSerializer and do the serialization manually, and benefit from serializing properties, and vice versa, deserializing them using constructors / private field assignment.