Reputation: 1013
Say we work with this class:
public class UsefulClass
{
public string A { get; set; }
public string B { get; set; }
public int? C { get; set; }
public int? D { get; set; }
public decimal E { get; set; }
public decimal F { get; set; }
}
Let's consider the following instance:
UsefulClass z_objUsefulInstance = new UsefulClass()
{
A = null,
C = null,
E = 0
};
At this point, z_objUsefulInstance.A
and C
are null
, E
is 0, B
, D
and F
have not been initialized.
Is there a way to tell, automatically, which properties of z_objUsefulInstance
haven't been initialized and which ones have been initialized with null
or 0?
EDIT: by popular demand, why I need this: to emulate a system of database access akin to EntityFramework. Right now all properties are a specific generic type, so it's rather easy to know which is null
and which is Generic<T>.HasNullValue == true
. But that generic type causes various issues and now we'd like to get rid of it, particularly as we have grown more conversant with Expression
s.
Upvotes: 0
Views: 961
Reputation: 16554
Is there a way to tell, automatically, which properties of z_objUsefulInstance haven't been initialized and which ones have been initialized with null or 0?
You can't really know in ways that you can easily inspect at runtime what properties have been set unless you intercept the property setter and set some sort of flag. from a first-principals perspective that would resemble something like this:
public class UsefulClass
{
public string A { get => _a; set { _a = value; A_Set = true; } }
private string _a;
private bool A_Set = false;
public string B { get => _b; set { _b = value; B_Set = true; } }
private string _b;
private bool B_Set = false;
public int? C { get => _c; set { _c = value; C_Set = true; } }
private string _c;
private bool C_Set = false;
public int? D { get => _d; set { _d = value; D_Set = true; } }
private string _d;
private bool D_Set = false;
public decimal E { get => _e; set { _e = value; E_Set = true; } }
private string _e;
private bool E_Set = false;
public decimal F { get => _f; set { _f = value; F_Set = true; } }
private string _f;
private bool F_Set = false;
}
It is pretty verbose, but you can see here how we are not comparing the value at all, we can determine definitively if each property has been set, thought not specifically during the initialization of the instance, this simple code only tracks if each property was set at all.
So after your init, we can inspect these new flags:
UsefulClass z_objUsefulInstance = new UsefulClass()
{
A = null,
C = null,
E = 0
};
Console.WriteLine(z.C_Set); // True
Console.WriteLine(z.D_Set); // False
We can simplify this with a dictionary for the backing store and helper methods to get
and set
the property values, we can even encapsulate that logic in a base class to make this easier to consume:
public class UsefulClass : PropertyTracker
{
public string A { get => GetProperty<string>(); set => SetProperty(value); }
public string B { get => GetProperty<string>(); set => SetProperty(value); }
public int? C { get => GetProperty<int?>(); set => SetProperty(value); }
public int? D { get => GetProperty<int?>(); set => SetProperty(value); }
public decimal E { get => GetProperty<decimal>(); set => SetProperty(value); }
public decimal F { get => GetProperty<decimal>(); set => SetProperty(value); }
}
public abstract class PropertyTracker
{
private Dictionary<string, object> _values = new Dictionary<string, object>();
protected void SetProperty<T>(T value, [System.Runtime.CompilerServices.CallerMemberName] string propertyName = null)
{
_values[propertyName] = value;
}
protected T GetProperty<T>([System.Runtime.CompilerServices.CallerMemberName] string propertyName = null)
{
if (!_values.ContainsKey(propertyName))
return default;
return (T)_values[propertyName];
}
public bool IsSet(string propertyName)
{
return _values.ContainsKey(propertyName);
}
}
See we still have the concept of a backing store, it's just not a field anymore. The inspection code is a bit different too:
UsefulClass z_objUsefulInstance = new UsefulClass()
{
A = null,
C = null,
E = 0
};
Console.WriteLine(z.IsSet(nameof(UsefulClass.C)); // True
Console.WriteLine(z.IsSet(nameof(UsefulClass.D)); // False
There are all sorts of techniques you can use to scaffold this or similar code out across your classes, this is just an example implementation. You could even write a generic wrapper that uses reflection to do the same thing. In my solutions I tend to use T4 templates to generate what are effectively View Model classes. My main argument was that I could generate some verbose code and take a hit at compile-time instead of a performance hit at runtime with a reflection based implementation.
If your ViewModel classes inherit from your model class, then you can get close to an apparently automatic implementation that is more compatible with the rest of your runtime, but that would require your properties be declared as virtual
to enable the inheriting class to override the implementation.
INotifyPropertyChanged
, or perhaps IChangeTracking
or IRevertibleChangeTracking
while you're there.Upvotes: 1
Reputation: 4929
UsefulClass z_objUsefulInstance = new UsefulClass() { A = null C = null, E = 0 };
At this point,
z_objUsefulInstance.A
andC
are null,E
is 0,B
,D
andF
have not been initialized.
No that's not quite right.
From "14.11.4 Constructor Execution" in the C#7 language spec
Variable initializers are transformed into assignment statements, and these assignment statements are executed before the invocation of the base class instance constructor.
So before your instance constructor in the above example is started executing, the properties are assigned
A = default(string); // null
B = default(string); // null
C = default(int?); // null
D = default(int?); // null
E = default(decimal); // 0.0m
F = default(decimal); // 0.0m
(Not quite accurate, but close enough for this answer)
Then your instance constructor is run (in this example, the default provided by the compiler), then your property assignments are made
A = null,
C = null,
E = 0
. There's no difference between E = 0
and E = default(decimal)
, nor is there a difference between null
and null
(default(string)).
If you need to tell whether a property was set or not you will have to provide a backing field, or otherwise control access to the property.
If you want to read more about constructor details, a friendlier summary than the language spec can be found at https://jonskeet.uk/csharp/constructors.html .
Upvotes: 1