Immutable builder and updaterImmutable Queue in Java using an Immutable StackBloch's Builder Pattern /...
Where was Carl Sagan working on a plan to detonate a nuke on the Moon? Where was he applying when he leaked it?
Notepad++ cannot print
How much does Commander Data weigh?
Who was president of the USA?
Can a Rogue PC teach an NPC to perform Sneak Attack?
How to respectfully refuse to assist co-workers with IT issues?
Network helper class with retry logic on failure
Do Bayesian credible intervals treat the estimated parameter as a random variable?
Two questions about typesetting a Roman missal
Did anyone try to find the little box that held Professor Moriarty and his wife after the crash?
How do I prevent other wifi networks from showing up on my computer?
How do I get toddlers to stop asking for food every hour?
Handling Disruptive Student on the Autistic Spectrum
Add newline to prompt if it's too long
Is gzip atomic?
Heyacrazy: No Diagonals
Prevent use of CNAME Record for Untrusted Domain
Does an atom recoil when photon radiate?
What verb is かまされる?
Compelling story with the world as a villain
Transposing from C to Cm?
Why do banks “park” their money at the European Central Bank?
Can I get temporary health insurance while moving to the US?
How do I, an introvert, communicate to my friend and only colleague, an extrovert, that I want to spend my scheduled breaks without them?
Immutable builder and updater
Immutable Queue in Java using an Immutable StackBloch's Builder Pattern / UpdaterInitializing immutable objects with a nested builderAlternative to the Josh Bloch Builder pattern in C#Generic immutable object builderReally simple immutable classSelective updates to immutable typesImmutable type updater using a special constructor
.everyoneloves__top-leaderboard:empty,.everyoneloves__mid-leaderboard:empty,.everyoneloves__bot-mid-leaderboard:empty{ margin-bottom:0;
}
$begingroup$
There aren't enough questions about creating immutable objects... so why not try it again with another approach. This time, it's a builder that maps properties to constructor parameters. Properties are selected via the With
method that also expects a new value for the parameter. Constructor parameters must match properties so it's primarily for DTOs that follow this pattern. Build
tries to find that constructor and creates the object.
// I need this primarily for the left-join of optional parameters.
internal class IgnoreCase : IEquatable<string>
{
public string Value { get; set; }
public bool Equals(string other) => StringComparer.OrdinalIgnoreCase.Equals(Value, other);
public override bool Equals(object obj) => obj is IgnoreCase ic && Equals(ic.Value);
public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value);
public static explicit operator IgnoreCase(string value) => new IgnoreCase { Value = value };
}
public class ImmutableBuilder<T>
{
private readonly IList<(MemberExpression Selector, object Value)> _selectors = new List<(MemberExpression Selector, object Value)>();
public ImmutableBuilder<T> With<TProperty>(Expression<Func<T, TProperty>> selector, TProperty value)
{
_selectors.Add(((MemberExpression)selector.Body, value));
return this;
}
public T Build()
{
var ctors =
from ctor in typeof(T).GetConstructors()
let parameters = ctor.GetParameters()
// Join parameters and values by parameter order.
// The ctor requires them sorted but they might be initialized in any order.
let requiredParameterValues =
from parameter in parameters.Where(p => !p.IsOptional)
join selector in _selectors on (IgnoreCase)parameter.Name equals (IgnoreCase)selector.Selector.Member.Name
select selector.Value
// Get optional parameters if any.
let optionalParameterValues =
from parameter in parameters.Where(p => p.IsOptional)
join selector in _selectors on (IgnoreCase)parameter.Name equals (IgnoreCase)selector.Selector.Member.Name into s
from selector in s.DefaultIfEmpty()
select selector.Value
// Make sure all required parameters are specified.
where requiredParameterValues.Count() == parameters.Where(p => !p.IsOptional).Count()
select (ctor, requiredParameterValues, optionalParameterValues);
var theOne = ctors.Single();
return (T)theOne.ctor.Invoke(theOne.requiredParameterValues.Concat(theOne.optionalParameterValues).ToArray());
}
}
public static class Immutable<T>
{
public static ImmutableBuilder<T> Builder => new ImmutableBuilder<T>();
}
In case we already have an immutable type and want to modify it by changing just some values, the ImmutableHelper
can be used. It also provides the With
method expecting a new value. Then matches it and other properties with the constructor and uses origianl values as defaults and for the specified property the new value.
public static class ImmutableHelper
{
public static T With<T, TProperty>(this T obj, Expression<Func<T, TProperty>> selector, TProperty value)
{
var comparer = StringComparer.OrdinalIgnoreCase;
var propertyName = ((MemberExpression)selector.Body).Member.Name;
var properties = typeof(T).GetProperties();
var propertyNames =
properties
.Select(x => x.Name)
// A hash-set is convenient for name matching in the next step.
.ToImmutableHashSet(comparer);
// Find the constructor that matches property names.
var ctors =
from ctor in typeof(T).GetConstructors()
where propertyNames.IsProperSupersetOf(ctor.GetParameters().Select(x => x.Name))
select ctor;
var theOne = ctors.Single(); // There can be only one match.
var parameters =
from parameter in theOne.GetParameters()
join property in properties on (IgnoreCase)parameter.Name equals (IgnoreCase)property.Name
// They definitely match so no comparer is necessary.
// Use either the new value or the current one.
select property.Name == propertyName ? value : property.GetValue(obj);
return (T)theOne.Invoke(parameters.ToArray());
}
}
Example
This is how it can be used:
void Main()
{
var person =
Immutable<Person>
.Builder
.With(x => x.FirstName, "Jane")
.With(x => x.LastName, null)
//.With(x => x.NickName, "JD") // Optional
.Build();
person.With(x => x.LastName, "Doe").Dump();
}
public class Person
{
public Person(string firstName, string lastName, string nickName = null)
{
FirstName = firstName;
LastName = lastName;
NickName = nickName;
}
// This ctor should confuse the API.
public Person(string other) { }
public string FirstName { get; }
public string LastName { get; }
public string NickName { get; }
// This property should confuse the API too.
public string FullName => $"{LastName}, {FirstName}";
}
What do you say? Crazy? Insane? I like it? Let's improve it?
c# generics reflection immutability expression-trees
$endgroup$
|
show 2 more comments
$begingroup$
There aren't enough questions about creating immutable objects... so why not try it again with another approach. This time, it's a builder that maps properties to constructor parameters. Properties are selected via the With
method that also expects a new value for the parameter. Constructor parameters must match properties so it's primarily for DTOs that follow this pattern. Build
tries to find that constructor and creates the object.
// I need this primarily for the left-join of optional parameters.
internal class IgnoreCase : IEquatable<string>
{
public string Value { get; set; }
public bool Equals(string other) => StringComparer.OrdinalIgnoreCase.Equals(Value, other);
public override bool Equals(object obj) => obj is IgnoreCase ic && Equals(ic.Value);
public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value);
public static explicit operator IgnoreCase(string value) => new IgnoreCase { Value = value };
}
public class ImmutableBuilder<T>
{
private readonly IList<(MemberExpression Selector, object Value)> _selectors = new List<(MemberExpression Selector, object Value)>();
public ImmutableBuilder<T> With<TProperty>(Expression<Func<T, TProperty>> selector, TProperty value)
{
_selectors.Add(((MemberExpression)selector.Body, value));
return this;
}
public T Build()
{
var ctors =
from ctor in typeof(T).GetConstructors()
let parameters = ctor.GetParameters()
// Join parameters and values by parameter order.
// The ctor requires them sorted but they might be initialized in any order.
let requiredParameterValues =
from parameter in parameters.Where(p => !p.IsOptional)
join selector in _selectors on (IgnoreCase)parameter.Name equals (IgnoreCase)selector.Selector.Member.Name
select selector.Value
// Get optional parameters if any.
let optionalParameterValues =
from parameter in parameters.Where(p => p.IsOptional)
join selector in _selectors on (IgnoreCase)parameter.Name equals (IgnoreCase)selector.Selector.Member.Name into s
from selector in s.DefaultIfEmpty()
select selector.Value
// Make sure all required parameters are specified.
where requiredParameterValues.Count() == parameters.Where(p => !p.IsOptional).Count()
select (ctor, requiredParameterValues, optionalParameterValues);
var theOne = ctors.Single();
return (T)theOne.ctor.Invoke(theOne.requiredParameterValues.Concat(theOne.optionalParameterValues).ToArray());
}
}
public static class Immutable<T>
{
public static ImmutableBuilder<T> Builder => new ImmutableBuilder<T>();
}
In case we already have an immutable type and want to modify it by changing just some values, the ImmutableHelper
can be used. It also provides the With
method expecting a new value. Then matches it and other properties with the constructor and uses origianl values as defaults and for the specified property the new value.
public static class ImmutableHelper
{
public static T With<T, TProperty>(this T obj, Expression<Func<T, TProperty>> selector, TProperty value)
{
var comparer = StringComparer.OrdinalIgnoreCase;
var propertyName = ((MemberExpression)selector.Body).Member.Name;
var properties = typeof(T).GetProperties();
var propertyNames =
properties
.Select(x => x.Name)
// A hash-set is convenient for name matching in the next step.
.ToImmutableHashSet(comparer);
// Find the constructor that matches property names.
var ctors =
from ctor in typeof(T).GetConstructors()
where propertyNames.IsProperSupersetOf(ctor.GetParameters().Select(x => x.Name))
select ctor;
var theOne = ctors.Single(); // There can be only one match.
var parameters =
from parameter in theOne.GetParameters()
join property in properties on (IgnoreCase)parameter.Name equals (IgnoreCase)property.Name
// They definitely match so no comparer is necessary.
// Use either the new value or the current one.
select property.Name == propertyName ? value : property.GetValue(obj);
return (T)theOne.Invoke(parameters.ToArray());
}
}
Example
This is how it can be used:
void Main()
{
var person =
Immutable<Person>
.Builder
.With(x => x.FirstName, "Jane")
.With(x => x.LastName, null)
//.With(x => x.NickName, "JD") // Optional
.Build();
person.With(x => x.LastName, "Doe").Dump();
}
public class Person
{
public Person(string firstName, string lastName, string nickName = null)
{
FirstName = firstName;
LastName = lastName;
NickName = nickName;
}
// This ctor should confuse the API.
public Person(string other) { }
public string FirstName { get; }
public string LastName { get; }
public string NickName { get; }
// This property should confuse the API too.
public string FullName => $"{LastName}, {FirstName}";
}
What do you say? Crazy? Insane? I like it? Let's improve it?
c# generics reflection immutability expression-trees
$endgroup$
2
$begingroup$
This has a use case for building controllers and what not that have dozens of parameters. Does this also work with optional ctr arguments?
$endgroup$
– dfhwze
yesterday
1
$begingroup$
@dfhwze it doesn't... but it definitelly could... and should.
$endgroup$
– t3chb0t
yesterday
$begingroup$
Ok ... so let's improve it! :p
$endgroup$
– dfhwze
yesterday
$begingroup$
@dfhwze done! ;-] at least the main builder can do this now.
$endgroup$
– t3chb0t
yesterday
1
$begingroup$
I like theWith
extensions method that helps to create modified copies of mutable types. However I didn't get the use case of theImmutableBuilder
.. Why not just calling the constructor?
$endgroup$
– JanDotNet
yesterday
|
show 2 more comments
$begingroup$
There aren't enough questions about creating immutable objects... so why not try it again with another approach. This time, it's a builder that maps properties to constructor parameters. Properties are selected via the With
method that also expects a new value for the parameter. Constructor parameters must match properties so it's primarily for DTOs that follow this pattern. Build
tries to find that constructor and creates the object.
// I need this primarily for the left-join of optional parameters.
internal class IgnoreCase : IEquatable<string>
{
public string Value { get; set; }
public bool Equals(string other) => StringComparer.OrdinalIgnoreCase.Equals(Value, other);
public override bool Equals(object obj) => obj is IgnoreCase ic && Equals(ic.Value);
public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value);
public static explicit operator IgnoreCase(string value) => new IgnoreCase { Value = value };
}
public class ImmutableBuilder<T>
{
private readonly IList<(MemberExpression Selector, object Value)> _selectors = new List<(MemberExpression Selector, object Value)>();
public ImmutableBuilder<T> With<TProperty>(Expression<Func<T, TProperty>> selector, TProperty value)
{
_selectors.Add(((MemberExpression)selector.Body, value));
return this;
}
public T Build()
{
var ctors =
from ctor in typeof(T).GetConstructors()
let parameters = ctor.GetParameters()
// Join parameters and values by parameter order.
// The ctor requires them sorted but they might be initialized in any order.
let requiredParameterValues =
from parameter in parameters.Where(p => !p.IsOptional)
join selector in _selectors on (IgnoreCase)parameter.Name equals (IgnoreCase)selector.Selector.Member.Name
select selector.Value
// Get optional parameters if any.
let optionalParameterValues =
from parameter in parameters.Where(p => p.IsOptional)
join selector in _selectors on (IgnoreCase)parameter.Name equals (IgnoreCase)selector.Selector.Member.Name into s
from selector in s.DefaultIfEmpty()
select selector.Value
// Make sure all required parameters are specified.
where requiredParameterValues.Count() == parameters.Where(p => !p.IsOptional).Count()
select (ctor, requiredParameterValues, optionalParameterValues);
var theOne = ctors.Single();
return (T)theOne.ctor.Invoke(theOne.requiredParameterValues.Concat(theOne.optionalParameterValues).ToArray());
}
}
public static class Immutable<T>
{
public static ImmutableBuilder<T> Builder => new ImmutableBuilder<T>();
}
In case we already have an immutable type and want to modify it by changing just some values, the ImmutableHelper
can be used. It also provides the With
method expecting a new value. Then matches it and other properties with the constructor and uses origianl values as defaults and for the specified property the new value.
public static class ImmutableHelper
{
public static T With<T, TProperty>(this T obj, Expression<Func<T, TProperty>> selector, TProperty value)
{
var comparer = StringComparer.OrdinalIgnoreCase;
var propertyName = ((MemberExpression)selector.Body).Member.Name;
var properties = typeof(T).GetProperties();
var propertyNames =
properties
.Select(x => x.Name)
// A hash-set is convenient for name matching in the next step.
.ToImmutableHashSet(comparer);
// Find the constructor that matches property names.
var ctors =
from ctor in typeof(T).GetConstructors()
where propertyNames.IsProperSupersetOf(ctor.GetParameters().Select(x => x.Name))
select ctor;
var theOne = ctors.Single(); // There can be only one match.
var parameters =
from parameter in theOne.GetParameters()
join property in properties on (IgnoreCase)parameter.Name equals (IgnoreCase)property.Name
// They definitely match so no comparer is necessary.
// Use either the new value or the current one.
select property.Name == propertyName ? value : property.GetValue(obj);
return (T)theOne.Invoke(parameters.ToArray());
}
}
Example
This is how it can be used:
void Main()
{
var person =
Immutable<Person>
.Builder
.With(x => x.FirstName, "Jane")
.With(x => x.LastName, null)
//.With(x => x.NickName, "JD") // Optional
.Build();
person.With(x => x.LastName, "Doe").Dump();
}
public class Person
{
public Person(string firstName, string lastName, string nickName = null)
{
FirstName = firstName;
LastName = lastName;
NickName = nickName;
}
// This ctor should confuse the API.
public Person(string other) { }
public string FirstName { get; }
public string LastName { get; }
public string NickName { get; }
// This property should confuse the API too.
public string FullName => $"{LastName}, {FirstName}";
}
What do you say? Crazy? Insane? I like it? Let's improve it?
c# generics reflection immutability expression-trees
$endgroup$
There aren't enough questions about creating immutable objects... so why not try it again with another approach. This time, it's a builder that maps properties to constructor parameters. Properties are selected via the With
method that also expects a new value for the parameter. Constructor parameters must match properties so it's primarily for DTOs that follow this pattern. Build
tries to find that constructor and creates the object.
// I need this primarily for the left-join of optional parameters.
internal class IgnoreCase : IEquatable<string>
{
public string Value { get; set; }
public bool Equals(string other) => StringComparer.OrdinalIgnoreCase.Equals(Value, other);
public override bool Equals(object obj) => obj is IgnoreCase ic && Equals(ic.Value);
public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value);
public static explicit operator IgnoreCase(string value) => new IgnoreCase { Value = value };
}
public class ImmutableBuilder<T>
{
private readonly IList<(MemberExpression Selector, object Value)> _selectors = new List<(MemberExpression Selector, object Value)>();
public ImmutableBuilder<T> With<TProperty>(Expression<Func<T, TProperty>> selector, TProperty value)
{
_selectors.Add(((MemberExpression)selector.Body, value));
return this;
}
public T Build()
{
var ctors =
from ctor in typeof(T).GetConstructors()
let parameters = ctor.GetParameters()
// Join parameters and values by parameter order.
// The ctor requires them sorted but they might be initialized in any order.
let requiredParameterValues =
from parameter in parameters.Where(p => !p.IsOptional)
join selector in _selectors on (IgnoreCase)parameter.Name equals (IgnoreCase)selector.Selector.Member.Name
select selector.Value
// Get optional parameters if any.
let optionalParameterValues =
from parameter in parameters.Where(p => p.IsOptional)
join selector in _selectors on (IgnoreCase)parameter.Name equals (IgnoreCase)selector.Selector.Member.Name into s
from selector in s.DefaultIfEmpty()
select selector.Value
// Make sure all required parameters are specified.
where requiredParameterValues.Count() == parameters.Where(p => !p.IsOptional).Count()
select (ctor, requiredParameterValues, optionalParameterValues);
var theOne = ctors.Single();
return (T)theOne.ctor.Invoke(theOne.requiredParameterValues.Concat(theOne.optionalParameterValues).ToArray());
}
}
public static class Immutable<T>
{
public static ImmutableBuilder<T> Builder => new ImmutableBuilder<T>();
}
In case we already have an immutable type and want to modify it by changing just some values, the ImmutableHelper
can be used. It also provides the With
method expecting a new value. Then matches it and other properties with the constructor and uses origianl values as defaults and for the specified property the new value.
public static class ImmutableHelper
{
public static T With<T, TProperty>(this T obj, Expression<Func<T, TProperty>> selector, TProperty value)
{
var comparer = StringComparer.OrdinalIgnoreCase;
var propertyName = ((MemberExpression)selector.Body).Member.Name;
var properties = typeof(T).GetProperties();
var propertyNames =
properties
.Select(x => x.Name)
// A hash-set is convenient for name matching in the next step.
.ToImmutableHashSet(comparer);
// Find the constructor that matches property names.
var ctors =
from ctor in typeof(T).GetConstructors()
where propertyNames.IsProperSupersetOf(ctor.GetParameters().Select(x => x.Name))
select ctor;
var theOne = ctors.Single(); // There can be only one match.
var parameters =
from parameter in theOne.GetParameters()
join property in properties on (IgnoreCase)parameter.Name equals (IgnoreCase)property.Name
// They definitely match so no comparer is necessary.
// Use either the new value or the current one.
select property.Name == propertyName ? value : property.GetValue(obj);
return (T)theOne.Invoke(parameters.ToArray());
}
}
Example
This is how it can be used:
void Main()
{
var person =
Immutable<Person>
.Builder
.With(x => x.FirstName, "Jane")
.With(x => x.LastName, null)
//.With(x => x.NickName, "JD") // Optional
.Build();
person.With(x => x.LastName, "Doe").Dump();
}
public class Person
{
public Person(string firstName, string lastName, string nickName = null)
{
FirstName = firstName;
LastName = lastName;
NickName = nickName;
}
// This ctor should confuse the API.
public Person(string other) { }
public string FirstName { get; }
public string LastName { get; }
public string NickName { get; }
// This property should confuse the API too.
public string FullName => $"{LastName}, {FirstName}";
}
What do you say? Crazy? Insane? I like it? Let's improve it?
c# generics reflection immutability expression-trees
c# generics reflection immutability expression-trees
edited yesterday
t3chb0t
asked yesterday
t3chb0tt3chb0t
37.8k7 gold badges60 silver badges141 bronze badges
37.8k7 gold badges60 silver badges141 bronze badges
2
$begingroup$
This has a use case for building controllers and what not that have dozens of parameters. Does this also work with optional ctr arguments?
$endgroup$
– dfhwze
yesterday
1
$begingroup$
@dfhwze it doesn't... but it definitelly could... and should.
$endgroup$
– t3chb0t
yesterday
$begingroup$
Ok ... so let's improve it! :p
$endgroup$
– dfhwze
yesterday
$begingroup$
@dfhwze done! ;-] at least the main builder can do this now.
$endgroup$
– t3chb0t
yesterday
1
$begingroup$
I like theWith
extensions method that helps to create modified copies of mutable types. However I didn't get the use case of theImmutableBuilder
.. Why not just calling the constructor?
$endgroup$
– JanDotNet
yesterday
|
show 2 more comments
2
$begingroup$
This has a use case for building controllers and what not that have dozens of parameters. Does this also work with optional ctr arguments?
$endgroup$
– dfhwze
yesterday
1
$begingroup$
@dfhwze it doesn't... but it definitelly could... and should.
$endgroup$
– t3chb0t
yesterday
$begingroup$
Ok ... so let's improve it! :p
$endgroup$
– dfhwze
yesterday
$begingroup$
@dfhwze done! ;-] at least the main builder can do this now.
$endgroup$
– t3chb0t
yesterday
1
$begingroup$
I like theWith
extensions method that helps to create modified copies of mutable types. However I didn't get the use case of theImmutableBuilder
.. Why not just calling the constructor?
$endgroup$
– JanDotNet
yesterday
2
2
$begingroup$
This has a use case for building controllers and what not that have dozens of parameters. Does this also work with optional ctr arguments?
$endgroup$
– dfhwze
yesterday
$begingroup$
This has a use case for building controllers and what not that have dozens of parameters. Does this also work with optional ctr arguments?
$endgroup$
– dfhwze
yesterday
1
1
$begingroup$
@dfhwze it doesn't... but it definitelly could... and should.
$endgroup$
– t3chb0t
yesterday
$begingroup$
@dfhwze it doesn't... but it definitelly could... and should.
$endgroup$
– t3chb0t
yesterday
$begingroup$
Ok ... so let's improve it! :p
$endgroup$
– dfhwze
yesterday
$begingroup$
Ok ... so let's improve it! :p
$endgroup$
– dfhwze
yesterday
$begingroup$
@dfhwze done! ;-] at least the main builder can do this now.
$endgroup$
– t3chb0t
yesterday
$begingroup$
@dfhwze done! ;-] at least the main builder can do this now.
$endgroup$
– t3chb0t
yesterday
1
1
$begingroup$
I like the
With
extensions method that helps to create modified copies of mutable types. However I didn't get the use case of the ImmutableBuilder
.. Why not just calling the constructor?$endgroup$
– JanDotNet
yesterday
$begingroup$
I like the
With
extensions method that helps to create modified copies of mutable types. However I didn't get the use case of the ImmutableBuilder
.. Why not just calling the constructor?$endgroup$
– JanDotNet
yesterday
|
show 2 more comments
2 Answers
2
active
oldest
votes
$begingroup$
You use IList<>
where you should use ICollection<>
. I've rarely encountered a scenario where IList<>
actually needs to be used. The ICollection<>
interface has most of the list's methods, but without everything related to indexing, which you don't use anyway. It's not that big of a deal, but I think it's good knowledge.
When you search for constructor parameters, I think you should match on parameter types in addition of parameter names. The reason behind this is that this that parameter types with names are guaranteed to be unique, where parameter names could not. For example (it's not a great one, but it's a case that would make your code potentially crash)
class Foobar
{
public Foobar(string a, int b)
{
}
public Foobar(string a, double b)
{
}
}
One problem I see with the Immutable<T>
class is that I wouldn't expect a static
property to return a new reference every time I call it. I've been trying to find another example in the .NET framework of when this happens, and... I couldn't. I would change it for a method named something like CreateBuilder()
or something like that, this way it's clear that we are using a new builder every time we call the method.
I think the Immutable<>
type is misleading. When seeing this, I'd expect being able to use it to make some mutable type immutable (however that would be done), but that's not what it does. As a matter of fact, most of your code doesn't rely on the T
type to be immutable, which makes me think a tweak or two could make your tool work on mutable types too. In that sense, claiming it's an ImmutableBuilder
is kind of wrong, it's a builder that works on immutable types, but on mutable types too.
According to comments, your With
method in ImmutableHelper
creates a copy of the object with a changed parameter, which is alright considering it doesn't modify the immutable type. What I think could be improved is a similar method with the signature static T With<T, TProperty>(this T obj, IEnmerable<(Expression<Func<T, TProperty>> selector, TProperty value)>)
, so that if you want to modify more than one field in the object you could do so without having to create a copy of the object every time.
$endgroup$
$begingroup$
About the last paragraph... this will go into production (after I made it match parameter types too) because let's say you have an immutable typeUri
with such a constructor asscheme, authority, path, query, fragment
and you just want to change itsscheme
from nothing (relative uri) tohttp
and maybe add afragment
. It's mutch easier and reliable to do this withWith(x => x.Property, value)
and let the framework handle other values than copy all the values yourself... and I once made that mistake of forgetting one of them... I was chasing the bug for 3h :-[
$endgroup$
– t3chb0t
yesterday
$begingroup$
@t3chb0t But by doing this you break everything that is expected from an immutable type. If another developer uses theUri
class, he/she will expect it to be immutable. If you happen to use your method somewhere in the code, debugging for this other person will be much more hellish than otherwise. You could use aCopy
method then change the necessary fields or something else, maybe.
$endgroup$
– IEatBagels
yesterday
1
$begingroup$
@t3chb0t Although I understand the bug you've had, it's a tricky bug to find because it's kind of unexpected. I just don't think mutating immutable types is the right solution. Otherwise, maybe yourWith
method could create a new copy of the object
$endgroup$
– IEatBagels
yesterday
$begingroup$
Oh, you mean this... yes! It is creating a new copy. The first class is just an initializer and the extensionImmutableHelper.With
copies values into a new object. I probably should have put quotes around updater.
$endgroup$
– t3chb0t
yesterday
$begingroup$
@t3chb0t oohhh. Let me edit my answer then.
$endgroup$
– IEatBagels
yesterday
|
show 2 more comments
$begingroup$
(self-answer)
The idea for not-using the constructor is indeed insane but... since it's possible I kept it in the new version too. I changed the name of this tool to DtoUpdater
. It now can collect updates for multiple properties that at the end have to be Commit
ed. Parameters are now matched with members not only by name but also by type and it picks the constructor with the most parameters ana matching properties. I created this helper for updating simple DTOs and they usually have only one constructor initializing all properties so I think its current complexity is sufficient for most use-cases.
This version also no longer forces the user to specify all values that a constructor requires. I think that this new functionality now makes this utility in certain situations more useful than using the construtor... if it allows default
values of course.
public static class DtoUpdater
{
public static DtoUpdater<T> For<T>() => new DtoUpdater<T>(default);
public static DtoUpdater<T> Update<T>(this T obj) => new DtoUpdater<T>(obj);
}
public class DtoUpdater<T>
{
private readonly T _obj;
private readonly ICollection<(MemberInfo Member, object Value)> _updates = new List<(MemberInfo Member, object Value)>();
public DtoUpdater(T obj) => _obj = obj;
public DtoUpdater<T> With<TProperty>(Expression<Func<T, TProperty>> update, TProperty value)
{
_updates.Add((((MemberExpression)update.Body).Member, value));
return this;
}
public T Commit()
{
var members =
from member in typeof(T).GetMembers(BindingFlags.Public | BindingFlags.Instance).Where(m => m is PropertyInfo || m is FieldInfo)
select (member.Name, Type: (member as PropertyInfo)?.PropertyType ?? (member as FieldInfo)?.FieldType);
members = members.ToList();
// Find the ctor that matches most properties.
var ctors =
from ctor in typeof(T).GetConstructors()
let parameters = ctor.GetParameters()
from parameter in parameters
join member in members
on
new
{
Name = parameter.Name.AsIgnoreCase(),
Type = parameter.ParameterType
}
equals
new
{
Name = member.Name.AsIgnoreCase(),
Type = member.Type
}
orderby parameters.Length descending
select ctor;
var theOne = ctors.First();
// Join parameters and values by parameter order.
// The ctor requires them sorted but they might be initialized in any order.
var parameterValues =
from parameter in theOne.GetParameters()
join update in _updates on parameter.Name.AsIgnoreCase() equals update.Member.Name.AsIgnoreCase() into x
from update in x.DefaultIfEmpty()
select update.Value ?? GetMemberValueOrDefault(parameter.Name);
return (T)theOne.Invoke(parameterValues.ToArray());
}
private object GetMemberValueOrDefault(string memberName)
{
if (_obj == null) return default;
// There is for sure only one member with that name.
switch (typeof(T).GetMembers(BindingFlags.Public | BindingFlags.Instance).Single(m => m.Name.AsIgnoreCase().Equals(memberName)))
{
case PropertyInfo p: return p.GetValue(_obj);
case FieldInfo f: return f.GetValue(_obj);
default: return default; // Makes the compiler very happy.
}
}
}
public static class StringExtensions
{
public static IEquatable<string> AsIgnoreCase(this string str) => (IgnoreCase)str;
private class IgnoreCase : IEquatable<string>
{
private IgnoreCase(string value) => Value = value;
private string Value { get; }
public bool Equals(string other) => StringComparer.OrdinalIgnoreCase.Equals(Value, other);
public override bool Equals(object obj) => obj is IgnoreCase ic && Equals(ic.Value);
public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value);
public static explicit operator IgnoreCase(string value) => new IgnoreCase(value);
}
}
I hid the IgnoreCase
helper behind a new extension:
public static class StringExtensions
{
public static IEquatable<string> AsIgnoreCase(this string str) => (IgnoreCase)str;
private class IgnoreCase : IEquatable<string>
{
private IgnoreCase(string value) => Value = value;
private string Value { get; }
public bool Equals(string other) => StringComparer.OrdinalIgnoreCase.Equals(Value, other);
public override bool Equals(object obj) => obj is IgnoreCase ic && Equals(ic.Value);
public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value);
public static explicit operator IgnoreCase(string value) => new IgnoreCase(value);
}
}
The new API can now be used like this:
public class DtoBuilderTest
{
[Fact]
public void Can_create_and_update_object()
{
var person =
DtoUpdater
.For<Person>()
.With(x => x.FirstName, "Jane")
.With(x => x.LastName, null)
//.With(x => x.NickName, "JD") // Optional
.Commit();
Assert.Equal("Jane", person.FirstName);
Assert.Null(person.LastName);
Assert.Null(person.NickName);
person =
person
.Update()
.With(x => x.LastName, "Doe")
.With(x => x.NickName, "JD")
.Commit();
Assert.Equal("Jane", person.FirstName);
Assert.Equal("Doe", person.LastName);
Assert.Equal("JD", person.NickName);
}
private class Person
{
public Person(string firstName, string lastName, string nickName = null)
{
FirstName = firstName;
LastName = lastName;
NickName = nickName;
}
// This ctor should confuse the API.
public Person(string other) { }
public string FirstName { get; }
public string LastName { get; }
public string NickName { get; set; }
// This property should confuse the API too.
public string FullName => $"{LastName}, {FirstName}";
}
}
$endgroup$
1
$begingroup$
This update/commit pattern makes me think we can tune this up to be used as a batch updater with support for multi-level change tracking/reverting implementing IEditableObject, IChangeTracking and IRevertibleChangeTracking (up-vote pending btw)
$endgroup$
– dfhwze
yesterday
$begingroup$
@dfhwze haha don't even think of it. Two more refactoring rounds and it can fly to the moon ;-P
$endgroup$
– t3chb0t
yesterday
$begingroup$
I might ask a follow-up question about it in the weekend o_0 But change tracking is not that trivial /o
$endgroup$
– dfhwze
yesterday
$begingroup$
@dfhwze I'm not so sure we need all this tracking and undo logic. This demo doesn't show that but usually when you modify an immutable, you pass it so some deeper scope. When it returns, you pick up the previous version for this scope and continue from here. At leat this is how I mostly use immutables. They are convenient to update and use in a new context for a moment. Although it might be useful for debugging.
$endgroup$
– t3chb0t
yesterday
1
$begingroup$
@dfhwze oh, now I get it. then please do. I could use one of those too. So we'll be borrowing from each other haha
$endgroup$
– t3chb0t
yesterday
|
show 1 more comment
Your Answer
StackExchange.ifUsing("editor", function () {
StackExchange.using("externalEditor", function () {
StackExchange.using("snippets", function () {
StackExchange.snippets.init();
});
});
}, "code-snippets");
StackExchange.ready(function() {
var channelOptions = {
tags: "".split(" "),
id: "196"
};
initTagRenderer("".split(" "), "".split(" "), channelOptions);
StackExchange.using("externalEditor", function() {
// Have to fire editor after snippets, if snippets enabled
if (StackExchange.settings.snippets.snippetsEnabled) {
StackExchange.using("snippets", function() {
createEditor();
});
}
else {
createEditor();
}
});
function createEditor() {
StackExchange.prepareEditor({
heartbeatType: 'answer',
autoActivateHeartbeat: false,
convertImagesToLinks: false,
noModals: true,
showLowRepImageUploadWarning: true,
reputationToPostImages: null,
bindNavPrevention: true,
postfix: "",
imageUploader: {
brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
allowUrls: true
},
onDemand: true,
discardSelector: ".discard-answer"
,immediatelyShowMarkdownHelp:true
});
}
});
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f226694%2fimmutable-builder-and-updater%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
2 Answers
2
active
oldest
votes
2 Answers
2
active
oldest
votes
active
oldest
votes
active
oldest
votes
$begingroup$
You use IList<>
where you should use ICollection<>
. I've rarely encountered a scenario where IList<>
actually needs to be used. The ICollection<>
interface has most of the list's methods, but without everything related to indexing, which you don't use anyway. It's not that big of a deal, but I think it's good knowledge.
When you search for constructor parameters, I think you should match on parameter types in addition of parameter names. The reason behind this is that this that parameter types with names are guaranteed to be unique, where parameter names could not. For example (it's not a great one, but it's a case that would make your code potentially crash)
class Foobar
{
public Foobar(string a, int b)
{
}
public Foobar(string a, double b)
{
}
}
One problem I see with the Immutable<T>
class is that I wouldn't expect a static
property to return a new reference every time I call it. I've been trying to find another example in the .NET framework of when this happens, and... I couldn't. I would change it for a method named something like CreateBuilder()
or something like that, this way it's clear that we are using a new builder every time we call the method.
I think the Immutable<>
type is misleading. When seeing this, I'd expect being able to use it to make some mutable type immutable (however that would be done), but that's not what it does. As a matter of fact, most of your code doesn't rely on the T
type to be immutable, which makes me think a tweak or two could make your tool work on mutable types too. In that sense, claiming it's an ImmutableBuilder
is kind of wrong, it's a builder that works on immutable types, but on mutable types too.
According to comments, your With
method in ImmutableHelper
creates a copy of the object with a changed parameter, which is alright considering it doesn't modify the immutable type. What I think could be improved is a similar method with the signature static T With<T, TProperty>(this T obj, IEnmerable<(Expression<Func<T, TProperty>> selector, TProperty value)>)
, so that if you want to modify more than one field in the object you could do so without having to create a copy of the object every time.
$endgroup$
$begingroup$
About the last paragraph... this will go into production (after I made it match parameter types too) because let's say you have an immutable typeUri
with such a constructor asscheme, authority, path, query, fragment
and you just want to change itsscheme
from nothing (relative uri) tohttp
and maybe add afragment
. It's mutch easier and reliable to do this withWith(x => x.Property, value)
and let the framework handle other values than copy all the values yourself... and I once made that mistake of forgetting one of them... I was chasing the bug for 3h :-[
$endgroup$
– t3chb0t
yesterday
$begingroup$
@t3chb0t But by doing this you break everything that is expected from an immutable type. If another developer uses theUri
class, he/she will expect it to be immutable. If you happen to use your method somewhere in the code, debugging for this other person will be much more hellish than otherwise. You could use aCopy
method then change the necessary fields or something else, maybe.
$endgroup$
– IEatBagels
yesterday
1
$begingroup$
@t3chb0t Although I understand the bug you've had, it's a tricky bug to find because it's kind of unexpected. I just don't think mutating immutable types is the right solution. Otherwise, maybe yourWith
method could create a new copy of the object
$endgroup$
– IEatBagels
yesterday
$begingroup$
Oh, you mean this... yes! It is creating a new copy. The first class is just an initializer and the extensionImmutableHelper.With
copies values into a new object. I probably should have put quotes around updater.
$endgroup$
– t3chb0t
yesterday
$begingroup$
@t3chb0t oohhh. Let me edit my answer then.
$endgroup$
– IEatBagels
yesterday
|
show 2 more comments
$begingroup$
You use IList<>
where you should use ICollection<>
. I've rarely encountered a scenario where IList<>
actually needs to be used. The ICollection<>
interface has most of the list's methods, but without everything related to indexing, which you don't use anyway. It's not that big of a deal, but I think it's good knowledge.
When you search for constructor parameters, I think you should match on parameter types in addition of parameter names. The reason behind this is that this that parameter types with names are guaranteed to be unique, where parameter names could not. For example (it's not a great one, but it's a case that would make your code potentially crash)
class Foobar
{
public Foobar(string a, int b)
{
}
public Foobar(string a, double b)
{
}
}
One problem I see with the Immutable<T>
class is that I wouldn't expect a static
property to return a new reference every time I call it. I've been trying to find another example in the .NET framework of when this happens, and... I couldn't. I would change it for a method named something like CreateBuilder()
or something like that, this way it's clear that we are using a new builder every time we call the method.
I think the Immutable<>
type is misleading. When seeing this, I'd expect being able to use it to make some mutable type immutable (however that would be done), but that's not what it does. As a matter of fact, most of your code doesn't rely on the T
type to be immutable, which makes me think a tweak or two could make your tool work on mutable types too. In that sense, claiming it's an ImmutableBuilder
is kind of wrong, it's a builder that works on immutable types, but on mutable types too.
According to comments, your With
method in ImmutableHelper
creates a copy of the object with a changed parameter, which is alright considering it doesn't modify the immutable type. What I think could be improved is a similar method with the signature static T With<T, TProperty>(this T obj, IEnmerable<(Expression<Func<T, TProperty>> selector, TProperty value)>)
, so that if you want to modify more than one field in the object you could do so without having to create a copy of the object every time.
$endgroup$
$begingroup$
About the last paragraph... this will go into production (after I made it match parameter types too) because let's say you have an immutable typeUri
with such a constructor asscheme, authority, path, query, fragment
and you just want to change itsscheme
from nothing (relative uri) tohttp
and maybe add afragment
. It's mutch easier and reliable to do this withWith(x => x.Property, value)
and let the framework handle other values than copy all the values yourself... and I once made that mistake of forgetting one of them... I was chasing the bug for 3h :-[
$endgroup$
– t3chb0t
yesterday
$begingroup$
@t3chb0t But by doing this you break everything that is expected from an immutable type. If another developer uses theUri
class, he/she will expect it to be immutable. If you happen to use your method somewhere in the code, debugging for this other person will be much more hellish than otherwise. You could use aCopy
method then change the necessary fields or something else, maybe.
$endgroup$
– IEatBagels
yesterday
1
$begingroup$
@t3chb0t Although I understand the bug you've had, it's a tricky bug to find because it's kind of unexpected. I just don't think mutating immutable types is the right solution. Otherwise, maybe yourWith
method could create a new copy of the object
$endgroup$
– IEatBagels
yesterday
$begingroup$
Oh, you mean this... yes! It is creating a new copy. The first class is just an initializer and the extensionImmutableHelper.With
copies values into a new object. I probably should have put quotes around updater.
$endgroup$
– t3chb0t
yesterday
$begingroup$
@t3chb0t oohhh. Let me edit my answer then.
$endgroup$
– IEatBagels
yesterday
|
show 2 more comments
$begingroup$
You use IList<>
where you should use ICollection<>
. I've rarely encountered a scenario where IList<>
actually needs to be used. The ICollection<>
interface has most of the list's methods, but without everything related to indexing, which you don't use anyway. It's not that big of a deal, but I think it's good knowledge.
When you search for constructor parameters, I think you should match on parameter types in addition of parameter names. The reason behind this is that this that parameter types with names are guaranteed to be unique, where parameter names could not. For example (it's not a great one, but it's a case that would make your code potentially crash)
class Foobar
{
public Foobar(string a, int b)
{
}
public Foobar(string a, double b)
{
}
}
One problem I see with the Immutable<T>
class is that I wouldn't expect a static
property to return a new reference every time I call it. I've been trying to find another example in the .NET framework of when this happens, and... I couldn't. I would change it for a method named something like CreateBuilder()
or something like that, this way it's clear that we are using a new builder every time we call the method.
I think the Immutable<>
type is misleading. When seeing this, I'd expect being able to use it to make some mutable type immutable (however that would be done), but that's not what it does. As a matter of fact, most of your code doesn't rely on the T
type to be immutable, which makes me think a tweak or two could make your tool work on mutable types too. In that sense, claiming it's an ImmutableBuilder
is kind of wrong, it's a builder that works on immutable types, but on mutable types too.
According to comments, your With
method in ImmutableHelper
creates a copy of the object with a changed parameter, which is alright considering it doesn't modify the immutable type. What I think could be improved is a similar method with the signature static T With<T, TProperty>(this T obj, IEnmerable<(Expression<Func<T, TProperty>> selector, TProperty value)>)
, so that if you want to modify more than one field in the object you could do so without having to create a copy of the object every time.
$endgroup$
You use IList<>
where you should use ICollection<>
. I've rarely encountered a scenario where IList<>
actually needs to be used. The ICollection<>
interface has most of the list's methods, but without everything related to indexing, which you don't use anyway. It's not that big of a deal, but I think it's good knowledge.
When you search for constructor parameters, I think you should match on parameter types in addition of parameter names. The reason behind this is that this that parameter types with names are guaranteed to be unique, where parameter names could not. For example (it's not a great one, but it's a case that would make your code potentially crash)
class Foobar
{
public Foobar(string a, int b)
{
}
public Foobar(string a, double b)
{
}
}
One problem I see with the Immutable<T>
class is that I wouldn't expect a static
property to return a new reference every time I call it. I've been trying to find another example in the .NET framework of when this happens, and... I couldn't. I would change it for a method named something like CreateBuilder()
or something like that, this way it's clear that we are using a new builder every time we call the method.
I think the Immutable<>
type is misleading. When seeing this, I'd expect being able to use it to make some mutable type immutable (however that would be done), but that's not what it does. As a matter of fact, most of your code doesn't rely on the T
type to be immutable, which makes me think a tweak or two could make your tool work on mutable types too. In that sense, claiming it's an ImmutableBuilder
is kind of wrong, it's a builder that works on immutable types, but on mutable types too.
According to comments, your With
method in ImmutableHelper
creates a copy of the object with a changed parameter, which is alright considering it doesn't modify the immutable type. What I think could be improved is a similar method with the signature static T With<T, TProperty>(this T obj, IEnmerable<(Expression<Func<T, TProperty>> selector, TProperty value)>)
, so that if you want to modify more than one field in the object you could do so without having to create a copy of the object every time.
edited yesterday
answered yesterday
IEatBagelsIEatBagels
9,9502 gold badges35 silver badges86 bronze badges
9,9502 gold badges35 silver badges86 bronze badges
$begingroup$
About the last paragraph... this will go into production (after I made it match parameter types too) because let's say you have an immutable typeUri
with such a constructor asscheme, authority, path, query, fragment
and you just want to change itsscheme
from nothing (relative uri) tohttp
and maybe add afragment
. It's mutch easier and reliable to do this withWith(x => x.Property, value)
and let the framework handle other values than copy all the values yourself... and I once made that mistake of forgetting one of them... I was chasing the bug for 3h :-[
$endgroup$
– t3chb0t
yesterday
$begingroup$
@t3chb0t But by doing this you break everything that is expected from an immutable type. If another developer uses theUri
class, he/she will expect it to be immutable. If you happen to use your method somewhere in the code, debugging for this other person will be much more hellish than otherwise. You could use aCopy
method then change the necessary fields or something else, maybe.
$endgroup$
– IEatBagels
yesterday
1
$begingroup$
@t3chb0t Although I understand the bug you've had, it's a tricky bug to find because it's kind of unexpected. I just don't think mutating immutable types is the right solution. Otherwise, maybe yourWith
method could create a new copy of the object
$endgroup$
– IEatBagels
yesterday
$begingroup$
Oh, you mean this... yes! It is creating a new copy. The first class is just an initializer and the extensionImmutableHelper.With
copies values into a new object. I probably should have put quotes around updater.
$endgroup$
– t3chb0t
yesterday
$begingroup$
@t3chb0t oohhh. Let me edit my answer then.
$endgroup$
– IEatBagels
yesterday
|
show 2 more comments
$begingroup$
About the last paragraph... this will go into production (after I made it match parameter types too) because let's say you have an immutable typeUri
with such a constructor asscheme, authority, path, query, fragment
and you just want to change itsscheme
from nothing (relative uri) tohttp
and maybe add afragment
. It's mutch easier and reliable to do this withWith(x => x.Property, value)
and let the framework handle other values than copy all the values yourself... and I once made that mistake of forgetting one of them... I was chasing the bug for 3h :-[
$endgroup$
– t3chb0t
yesterday
$begingroup$
@t3chb0t But by doing this you break everything that is expected from an immutable type. If another developer uses theUri
class, he/she will expect it to be immutable. If you happen to use your method somewhere in the code, debugging for this other person will be much more hellish than otherwise. You could use aCopy
method then change the necessary fields or something else, maybe.
$endgroup$
– IEatBagels
yesterday
1
$begingroup$
@t3chb0t Although I understand the bug you've had, it's a tricky bug to find because it's kind of unexpected. I just don't think mutating immutable types is the right solution. Otherwise, maybe yourWith
method could create a new copy of the object
$endgroup$
– IEatBagels
yesterday
$begingroup$
Oh, you mean this... yes! It is creating a new copy. The first class is just an initializer and the extensionImmutableHelper.With
copies values into a new object. I probably should have put quotes around updater.
$endgroup$
– t3chb0t
yesterday
$begingroup$
@t3chb0t oohhh. Let me edit my answer then.
$endgroup$
– IEatBagels
yesterday
$begingroup$
About the last paragraph... this will go into production (after I made it match parameter types too) because let's say you have an immutable type
Uri
with such a constructor as scheme, authority, path, query, fragment
and you just want to change its scheme
from nothing (relative uri) to http
and maybe add a fragment
. It's mutch easier and reliable to do this with With(x => x.Property, value)
and let the framework handle other values than copy all the values yourself... and I once made that mistake of forgetting one of them... I was chasing the bug for 3h :-[$endgroup$
– t3chb0t
yesterday
$begingroup$
About the last paragraph... this will go into production (after I made it match parameter types too) because let's say you have an immutable type
Uri
with such a constructor as scheme, authority, path, query, fragment
and you just want to change its scheme
from nothing (relative uri) to http
and maybe add a fragment
. It's mutch easier and reliable to do this with With(x => x.Property, value)
and let the framework handle other values than copy all the values yourself... and I once made that mistake of forgetting one of them... I was chasing the bug for 3h :-[$endgroup$
– t3chb0t
yesterday
$begingroup$
@t3chb0t But by doing this you break everything that is expected from an immutable type. If another developer uses the
Uri
class, he/she will expect it to be immutable. If you happen to use your method somewhere in the code, debugging for this other person will be much more hellish than otherwise. You could use a Copy
method then change the necessary fields or something else, maybe.$endgroup$
– IEatBagels
yesterday
$begingroup$
@t3chb0t But by doing this you break everything that is expected from an immutable type. If another developer uses the
Uri
class, he/she will expect it to be immutable. If you happen to use your method somewhere in the code, debugging for this other person will be much more hellish than otherwise. You could use a Copy
method then change the necessary fields or something else, maybe.$endgroup$
– IEatBagels
yesterday
1
1
$begingroup$
@t3chb0t Although I understand the bug you've had, it's a tricky bug to find because it's kind of unexpected. I just don't think mutating immutable types is the right solution. Otherwise, maybe your
With
method could create a new copy of the object$endgroup$
– IEatBagels
yesterday
$begingroup$
@t3chb0t Although I understand the bug you've had, it's a tricky bug to find because it's kind of unexpected. I just don't think mutating immutable types is the right solution. Otherwise, maybe your
With
method could create a new copy of the object$endgroup$
– IEatBagels
yesterday
$begingroup$
Oh, you mean this... yes! It is creating a new copy. The first class is just an initializer and the extension
ImmutableHelper.With
copies values into a new object. I probably should have put quotes around updater.$endgroup$
– t3chb0t
yesterday
$begingroup$
Oh, you mean this... yes! It is creating a new copy. The first class is just an initializer and the extension
ImmutableHelper.With
copies values into a new object. I probably should have put quotes around updater.$endgroup$
– t3chb0t
yesterday
$begingroup$
@t3chb0t oohhh. Let me edit my answer then.
$endgroup$
– IEatBagels
yesterday
$begingroup$
@t3chb0t oohhh. Let me edit my answer then.
$endgroup$
– IEatBagels
yesterday
|
show 2 more comments
$begingroup$
(self-answer)
The idea for not-using the constructor is indeed insane but... since it's possible I kept it in the new version too. I changed the name of this tool to DtoUpdater
. It now can collect updates for multiple properties that at the end have to be Commit
ed. Parameters are now matched with members not only by name but also by type and it picks the constructor with the most parameters ana matching properties. I created this helper for updating simple DTOs and they usually have only one constructor initializing all properties so I think its current complexity is sufficient for most use-cases.
This version also no longer forces the user to specify all values that a constructor requires. I think that this new functionality now makes this utility in certain situations more useful than using the construtor... if it allows default
values of course.
public static class DtoUpdater
{
public static DtoUpdater<T> For<T>() => new DtoUpdater<T>(default);
public static DtoUpdater<T> Update<T>(this T obj) => new DtoUpdater<T>(obj);
}
public class DtoUpdater<T>
{
private readonly T _obj;
private readonly ICollection<(MemberInfo Member, object Value)> _updates = new List<(MemberInfo Member, object Value)>();
public DtoUpdater(T obj) => _obj = obj;
public DtoUpdater<T> With<TProperty>(Expression<Func<T, TProperty>> update, TProperty value)
{
_updates.Add((((MemberExpression)update.Body).Member, value));
return this;
}
public T Commit()
{
var members =
from member in typeof(T).GetMembers(BindingFlags.Public | BindingFlags.Instance).Where(m => m is PropertyInfo || m is FieldInfo)
select (member.Name, Type: (member as PropertyInfo)?.PropertyType ?? (member as FieldInfo)?.FieldType);
members = members.ToList();
// Find the ctor that matches most properties.
var ctors =
from ctor in typeof(T).GetConstructors()
let parameters = ctor.GetParameters()
from parameter in parameters
join member in members
on
new
{
Name = parameter.Name.AsIgnoreCase(),
Type = parameter.ParameterType
}
equals
new
{
Name = member.Name.AsIgnoreCase(),
Type = member.Type
}
orderby parameters.Length descending
select ctor;
var theOne = ctors.First();
// Join parameters and values by parameter order.
// The ctor requires them sorted but they might be initialized in any order.
var parameterValues =
from parameter in theOne.GetParameters()
join update in _updates on parameter.Name.AsIgnoreCase() equals update.Member.Name.AsIgnoreCase() into x
from update in x.DefaultIfEmpty()
select update.Value ?? GetMemberValueOrDefault(parameter.Name);
return (T)theOne.Invoke(parameterValues.ToArray());
}
private object GetMemberValueOrDefault(string memberName)
{
if (_obj == null) return default;
// There is for sure only one member with that name.
switch (typeof(T).GetMembers(BindingFlags.Public | BindingFlags.Instance).Single(m => m.Name.AsIgnoreCase().Equals(memberName)))
{
case PropertyInfo p: return p.GetValue(_obj);
case FieldInfo f: return f.GetValue(_obj);
default: return default; // Makes the compiler very happy.
}
}
}
public static class StringExtensions
{
public static IEquatable<string> AsIgnoreCase(this string str) => (IgnoreCase)str;
private class IgnoreCase : IEquatable<string>
{
private IgnoreCase(string value) => Value = value;
private string Value { get; }
public bool Equals(string other) => StringComparer.OrdinalIgnoreCase.Equals(Value, other);
public override bool Equals(object obj) => obj is IgnoreCase ic && Equals(ic.Value);
public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value);
public static explicit operator IgnoreCase(string value) => new IgnoreCase(value);
}
}
I hid the IgnoreCase
helper behind a new extension:
public static class StringExtensions
{
public static IEquatable<string> AsIgnoreCase(this string str) => (IgnoreCase)str;
private class IgnoreCase : IEquatable<string>
{
private IgnoreCase(string value) => Value = value;
private string Value { get; }
public bool Equals(string other) => StringComparer.OrdinalIgnoreCase.Equals(Value, other);
public override bool Equals(object obj) => obj is IgnoreCase ic && Equals(ic.Value);
public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value);
public static explicit operator IgnoreCase(string value) => new IgnoreCase(value);
}
}
The new API can now be used like this:
public class DtoBuilderTest
{
[Fact]
public void Can_create_and_update_object()
{
var person =
DtoUpdater
.For<Person>()
.With(x => x.FirstName, "Jane")
.With(x => x.LastName, null)
//.With(x => x.NickName, "JD") // Optional
.Commit();
Assert.Equal("Jane", person.FirstName);
Assert.Null(person.LastName);
Assert.Null(person.NickName);
person =
person
.Update()
.With(x => x.LastName, "Doe")
.With(x => x.NickName, "JD")
.Commit();
Assert.Equal("Jane", person.FirstName);
Assert.Equal("Doe", person.LastName);
Assert.Equal("JD", person.NickName);
}
private class Person
{
public Person(string firstName, string lastName, string nickName = null)
{
FirstName = firstName;
LastName = lastName;
NickName = nickName;
}
// This ctor should confuse the API.
public Person(string other) { }
public string FirstName { get; }
public string LastName { get; }
public string NickName { get; set; }
// This property should confuse the API too.
public string FullName => $"{LastName}, {FirstName}";
}
}
$endgroup$
1
$begingroup$
This update/commit pattern makes me think we can tune this up to be used as a batch updater with support for multi-level change tracking/reverting implementing IEditableObject, IChangeTracking and IRevertibleChangeTracking (up-vote pending btw)
$endgroup$
– dfhwze
yesterday
$begingroup$
@dfhwze haha don't even think of it. Two more refactoring rounds and it can fly to the moon ;-P
$endgroup$
– t3chb0t
yesterday
$begingroup$
I might ask a follow-up question about it in the weekend o_0 But change tracking is not that trivial /o
$endgroup$
– dfhwze
yesterday
$begingroup$
@dfhwze I'm not so sure we need all this tracking and undo logic. This demo doesn't show that but usually when you modify an immutable, you pass it so some deeper scope. When it returns, you pick up the previous version for this scope and continue from here. At leat this is how I mostly use immutables. They are convenient to update and use in a new context for a moment. Although it might be useful for debugging.
$endgroup$
– t3chb0t
yesterday
1
$begingroup$
@dfhwze oh, now I get it. then please do. I could use one of those too. So we'll be borrowing from each other haha
$endgroup$
– t3chb0t
yesterday
|
show 1 more comment
$begingroup$
(self-answer)
The idea for not-using the constructor is indeed insane but... since it's possible I kept it in the new version too. I changed the name of this tool to DtoUpdater
. It now can collect updates for multiple properties that at the end have to be Commit
ed. Parameters are now matched with members not only by name but also by type and it picks the constructor with the most parameters ana matching properties. I created this helper for updating simple DTOs and they usually have only one constructor initializing all properties so I think its current complexity is sufficient for most use-cases.
This version also no longer forces the user to specify all values that a constructor requires. I think that this new functionality now makes this utility in certain situations more useful than using the construtor... if it allows default
values of course.
public static class DtoUpdater
{
public static DtoUpdater<T> For<T>() => new DtoUpdater<T>(default);
public static DtoUpdater<T> Update<T>(this T obj) => new DtoUpdater<T>(obj);
}
public class DtoUpdater<T>
{
private readonly T _obj;
private readonly ICollection<(MemberInfo Member, object Value)> _updates = new List<(MemberInfo Member, object Value)>();
public DtoUpdater(T obj) => _obj = obj;
public DtoUpdater<T> With<TProperty>(Expression<Func<T, TProperty>> update, TProperty value)
{
_updates.Add((((MemberExpression)update.Body).Member, value));
return this;
}
public T Commit()
{
var members =
from member in typeof(T).GetMembers(BindingFlags.Public | BindingFlags.Instance).Where(m => m is PropertyInfo || m is FieldInfo)
select (member.Name, Type: (member as PropertyInfo)?.PropertyType ?? (member as FieldInfo)?.FieldType);
members = members.ToList();
// Find the ctor that matches most properties.
var ctors =
from ctor in typeof(T).GetConstructors()
let parameters = ctor.GetParameters()
from parameter in parameters
join member in members
on
new
{
Name = parameter.Name.AsIgnoreCase(),
Type = parameter.ParameterType
}
equals
new
{
Name = member.Name.AsIgnoreCase(),
Type = member.Type
}
orderby parameters.Length descending
select ctor;
var theOne = ctors.First();
// Join parameters and values by parameter order.
// The ctor requires them sorted but they might be initialized in any order.
var parameterValues =
from parameter in theOne.GetParameters()
join update in _updates on parameter.Name.AsIgnoreCase() equals update.Member.Name.AsIgnoreCase() into x
from update in x.DefaultIfEmpty()
select update.Value ?? GetMemberValueOrDefault(parameter.Name);
return (T)theOne.Invoke(parameterValues.ToArray());
}
private object GetMemberValueOrDefault(string memberName)
{
if (_obj == null) return default;
// There is for sure only one member with that name.
switch (typeof(T).GetMembers(BindingFlags.Public | BindingFlags.Instance).Single(m => m.Name.AsIgnoreCase().Equals(memberName)))
{
case PropertyInfo p: return p.GetValue(_obj);
case FieldInfo f: return f.GetValue(_obj);
default: return default; // Makes the compiler very happy.
}
}
}
public static class StringExtensions
{
public static IEquatable<string> AsIgnoreCase(this string str) => (IgnoreCase)str;
private class IgnoreCase : IEquatable<string>
{
private IgnoreCase(string value) => Value = value;
private string Value { get; }
public bool Equals(string other) => StringComparer.OrdinalIgnoreCase.Equals(Value, other);
public override bool Equals(object obj) => obj is IgnoreCase ic && Equals(ic.Value);
public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value);
public static explicit operator IgnoreCase(string value) => new IgnoreCase(value);
}
}
I hid the IgnoreCase
helper behind a new extension:
public static class StringExtensions
{
public static IEquatable<string> AsIgnoreCase(this string str) => (IgnoreCase)str;
private class IgnoreCase : IEquatable<string>
{
private IgnoreCase(string value) => Value = value;
private string Value { get; }
public bool Equals(string other) => StringComparer.OrdinalIgnoreCase.Equals(Value, other);
public override bool Equals(object obj) => obj is IgnoreCase ic && Equals(ic.Value);
public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value);
public static explicit operator IgnoreCase(string value) => new IgnoreCase(value);
}
}
The new API can now be used like this:
public class DtoBuilderTest
{
[Fact]
public void Can_create_and_update_object()
{
var person =
DtoUpdater
.For<Person>()
.With(x => x.FirstName, "Jane")
.With(x => x.LastName, null)
//.With(x => x.NickName, "JD") // Optional
.Commit();
Assert.Equal("Jane", person.FirstName);
Assert.Null(person.LastName);
Assert.Null(person.NickName);
person =
person
.Update()
.With(x => x.LastName, "Doe")
.With(x => x.NickName, "JD")
.Commit();
Assert.Equal("Jane", person.FirstName);
Assert.Equal("Doe", person.LastName);
Assert.Equal("JD", person.NickName);
}
private class Person
{
public Person(string firstName, string lastName, string nickName = null)
{
FirstName = firstName;
LastName = lastName;
NickName = nickName;
}
// This ctor should confuse the API.
public Person(string other) { }
public string FirstName { get; }
public string LastName { get; }
public string NickName { get; set; }
// This property should confuse the API too.
public string FullName => $"{LastName}, {FirstName}";
}
}
$endgroup$
1
$begingroup$
This update/commit pattern makes me think we can tune this up to be used as a batch updater with support for multi-level change tracking/reverting implementing IEditableObject, IChangeTracking and IRevertibleChangeTracking (up-vote pending btw)
$endgroup$
– dfhwze
yesterday
$begingroup$
@dfhwze haha don't even think of it. Two more refactoring rounds and it can fly to the moon ;-P
$endgroup$
– t3chb0t
yesterday
$begingroup$
I might ask a follow-up question about it in the weekend o_0 But change tracking is not that trivial /o
$endgroup$
– dfhwze
yesterday
$begingroup$
@dfhwze I'm not so sure we need all this tracking and undo logic. This demo doesn't show that but usually when you modify an immutable, you pass it so some deeper scope. When it returns, you pick up the previous version for this scope and continue from here. At leat this is how I mostly use immutables. They are convenient to update and use in a new context for a moment. Although it might be useful for debugging.
$endgroup$
– t3chb0t
yesterday
1
$begingroup$
@dfhwze oh, now I get it. then please do. I could use one of those too. So we'll be borrowing from each other haha
$endgroup$
– t3chb0t
yesterday
|
show 1 more comment
$begingroup$
(self-answer)
The idea for not-using the constructor is indeed insane but... since it's possible I kept it in the new version too. I changed the name of this tool to DtoUpdater
. It now can collect updates for multiple properties that at the end have to be Commit
ed. Parameters are now matched with members not only by name but also by type and it picks the constructor with the most parameters ana matching properties. I created this helper for updating simple DTOs and they usually have only one constructor initializing all properties so I think its current complexity is sufficient for most use-cases.
This version also no longer forces the user to specify all values that a constructor requires. I think that this new functionality now makes this utility in certain situations more useful than using the construtor... if it allows default
values of course.
public static class DtoUpdater
{
public static DtoUpdater<T> For<T>() => new DtoUpdater<T>(default);
public static DtoUpdater<T> Update<T>(this T obj) => new DtoUpdater<T>(obj);
}
public class DtoUpdater<T>
{
private readonly T _obj;
private readonly ICollection<(MemberInfo Member, object Value)> _updates = new List<(MemberInfo Member, object Value)>();
public DtoUpdater(T obj) => _obj = obj;
public DtoUpdater<T> With<TProperty>(Expression<Func<T, TProperty>> update, TProperty value)
{
_updates.Add((((MemberExpression)update.Body).Member, value));
return this;
}
public T Commit()
{
var members =
from member in typeof(T).GetMembers(BindingFlags.Public | BindingFlags.Instance).Where(m => m is PropertyInfo || m is FieldInfo)
select (member.Name, Type: (member as PropertyInfo)?.PropertyType ?? (member as FieldInfo)?.FieldType);
members = members.ToList();
// Find the ctor that matches most properties.
var ctors =
from ctor in typeof(T).GetConstructors()
let parameters = ctor.GetParameters()
from parameter in parameters
join member in members
on
new
{
Name = parameter.Name.AsIgnoreCase(),
Type = parameter.ParameterType
}
equals
new
{
Name = member.Name.AsIgnoreCase(),
Type = member.Type
}
orderby parameters.Length descending
select ctor;
var theOne = ctors.First();
// Join parameters and values by parameter order.
// The ctor requires them sorted but they might be initialized in any order.
var parameterValues =
from parameter in theOne.GetParameters()
join update in _updates on parameter.Name.AsIgnoreCase() equals update.Member.Name.AsIgnoreCase() into x
from update in x.DefaultIfEmpty()
select update.Value ?? GetMemberValueOrDefault(parameter.Name);
return (T)theOne.Invoke(parameterValues.ToArray());
}
private object GetMemberValueOrDefault(string memberName)
{
if (_obj == null) return default;
// There is for sure only one member with that name.
switch (typeof(T).GetMembers(BindingFlags.Public | BindingFlags.Instance).Single(m => m.Name.AsIgnoreCase().Equals(memberName)))
{
case PropertyInfo p: return p.GetValue(_obj);
case FieldInfo f: return f.GetValue(_obj);
default: return default; // Makes the compiler very happy.
}
}
}
public static class StringExtensions
{
public static IEquatable<string> AsIgnoreCase(this string str) => (IgnoreCase)str;
private class IgnoreCase : IEquatable<string>
{
private IgnoreCase(string value) => Value = value;
private string Value { get; }
public bool Equals(string other) => StringComparer.OrdinalIgnoreCase.Equals(Value, other);
public override bool Equals(object obj) => obj is IgnoreCase ic && Equals(ic.Value);
public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value);
public static explicit operator IgnoreCase(string value) => new IgnoreCase(value);
}
}
I hid the IgnoreCase
helper behind a new extension:
public static class StringExtensions
{
public static IEquatable<string> AsIgnoreCase(this string str) => (IgnoreCase)str;
private class IgnoreCase : IEquatable<string>
{
private IgnoreCase(string value) => Value = value;
private string Value { get; }
public bool Equals(string other) => StringComparer.OrdinalIgnoreCase.Equals(Value, other);
public override bool Equals(object obj) => obj is IgnoreCase ic && Equals(ic.Value);
public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value);
public static explicit operator IgnoreCase(string value) => new IgnoreCase(value);
}
}
The new API can now be used like this:
public class DtoBuilderTest
{
[Fact]
public void Can_create_and_update_object()
{
var person =
DtoUpdater
.For<Person>()
.With(x => x.FirstName, "Jane")
.With(x => x.LastName, null)
//.With(x => x.NickName, "JD") // Optional
.Commit();
Assert.Equal("Jane", person.FirstName);
Assert.Null(person.LastName);
Assert.Null(person.NickName);
person =
person
.Update()
.With(x => x.LastName, "Doe")
.With(x => x.NickName, "JD")
.Commit();
Assert.Equal("Jane", person.FirstName);
Assert.Equal("Doe", person.LastName);
Assert.Equal("JD", person.NickName);
}
private class Person
{
public Person(string firstName, string lastName, string nickName = null)
{
FirstName = firstName;
LastName = lastName;
NickName = nickName;
}
// This ctor should confuse the API.
public Person(string other) { }
public string FirstName { get; }
public string LastName { get; }
public string NickName { get; set; }
// This property should confuse the API too.
public string FullName => $"{LastName}, {FirstName}";
}
}
$endgroup$
(self-answer)
The idea for not-using the constructor is indeed insane but... since it's possible I kept it in the new version too. I changed the name of this tool to DtoUpdater
. It now can collect updates for multiple properties that at the end have to be Commit
ed. Parameters are now matched with members not only by name but also by type and it picks the constructor with the most parameters ana matching properties. I created this helper for updating simple DTOs and they usually have only one constructor initializing all properties so I think its current complexity is sufficient for most use-cases.
This version also no longer forces the user to specify all values that a constructor requires. I think that this new functionality now makes this utility in certain situations more useful than using the construtor... if it allows default
values of course.
public static class DtoUpdater
{
public static DtoUpdater<T> For<T>() => new DtoUpdater<T>(default);
public static DtoUpdater<T> Update<T>(this T obj) => new DtoUpdater<T>(obj);
}
public class DtoUpdater<T>
{
private readonly T _obj;
private readonly ICollection<(MemberInfo Member, object Value)> _updates = new List<(MemberInfo Member, object Value)>();
public DtoUpdater(T obj) => _obj = obj;
public DtoUpdater<T> With<TProperty>(Expression<Func<T, TProperty>> update, TProperty value)
{
_updates.Add((((MemberExpression)update.Body).Member, value));
return this;
}
public T Commit()
{
var members =
from member in typeof(T).GetMembers(BindingFlags.Public | BindingFlags.Instance).Where(m => m is PropertyInfo || m is FieldInfo)
select (member.Name, Type: (member as PropertyInfo)?.PropertyType ?? (member as FieldInfo)?.FieldType);
members = members.ToList();
// Find the ctor that matches most properties.
var ctors =
from ctor in typeof(T).GetConstructors()
let parameters = ctor.GetParameters()
from parameter in parameters
join member in members
on
new
{
Name = parameter.Name.AsIgnoreCase(),
Type = parameter.ParameterType
}
equals
new
{
Name = member.Name.AsIgnoreCase(),
Type = member.Type
}
orderby parameters.Length descending
select ctor;
var theOne = ctors.First();
// Join parameters and values by parameter order.
// The ctor requires them sorted but they might be initialized in any order.
var parameterValues =
from parameter in theOne.GetParameters()
join update in _updates on parameter.Name.AsIgnoreCase() equals update.Member.Name.AsIgnoreCase() into x
from update in x.DefaultIfEmpty()
select update.Value ?? GetMemberValueOrDefault(parameter.Name);
return (T)theOne.Invoke(parameterValues.ToArray());
}
private object GetMemberValueOrDefault(string memberName)
{
if (_obj == null) return default;
// There is for sure only one member with that name.
switch (typeof(T).GetMembers(BindingFlags.Public | BindingFlags.Instance).Single(m => m.Name.AsIgnoreCase().Equals(memberName)))
{
case PropertyInfo p: return p.GetValue(_obj);
case FieldInfo f: return f.GetValue(_obj);
default: return default; // Makes the compiler very happy.
}
}
}
public static class StringExtensions
{
public static IEquatable<string> AsIgnoreCase(this string str) => (IgnoreCase)str;
private class IgnoreCase : IEquatable<string>
{
private IgnoreCase(string value) => Value = value;
private string Value { get; }
public bool Equals(string other) => StringComparer.OrdinalIgnoreCase.Equals(Value, other);
public override bool Equals(object obj) => obj is IgnoreCase ic && Equals(ic.Value);
public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value);
public static explicit operator IgnoreCase(string value) => new IgnoreCase(value);
}
}
I hid the IgnoreCase
helper behind a new extension:
public static class StringExtensions
{
public static IEquatable<string> AsIgnoreCase(this string str) => (IgnoreCase)str;
private class IgnoreCase : IEquatable<string>
{
private IgnoreCase(string value) => Value = value;
private string Value { get; }
public bool Equals(string other) => StringComparer.OrdinalIgnoreCase.Equals(Value, other);
public override bool Equals(object obj) => obj is IgnoreCase ic && Equals(ic.Value);
public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value);
public static explicit operator IgnoreCase(string value) => new IgnoreCase(value);
}
}
The new API can now be used like this:
public class DtoBuilderTest
{
[Fact]
public void Can_create_and_update_object()
{
var person =
DtoUpdater
.For<Person>()
.With(x => x.FirstName, "Jane")
.With(x => x.LastName, null)
//.With(x => x.NickName, "JD") // Optional
.Commit();
Assert.Equal("Jane", person.FirstName);
Assert.Null(person.LastName);
Assert.Null(person.NickName);
person =
person
.Update()
.With(x => x.LastName, "Doe")
.With(x => x.NickName, "JD")
.Commit();
Assert.Equal("Jane", person.FirstName);
Assert.Equal("Doe", person.LastName);
Assert.Equal("JD", person.NickName);
}
private class Person
{
public Person(string firstName, string lastName, string nickName = null)
{
FirstName = firstName;
LastName = lastName;
NickName = nickName;
}
// This ctor should confuse the API.
public Person(string other) { }
public string FirstName { get; }
public string LastName { get; }
public string NickName { get; set; }
// This property should confuse the API too.
public string FullName => $"{LastName}, {FirstName}";
}
}
answered yesterday
t3chb0tt3chb0t
37.8k7 gold badges60 silver badges141 bronze badges
37.8k7 gold badges60 silver badges141 bronze badges
1
$begingroup$
This update/commit pattern makes me think we can tune this up to be used as a batch updater with support for multi-level change tracking/reverting implementing IEditableObject, IChangeTracking and IRevertibleChangeTracking (up-vote pending btw)
$endgroup$
– dfhwze
yesterday
$begingroup$
@dfhwze haha don't even think of it. Two more refactoring rounds and it can fly to the moon ;-P
$endgroup$
– t3chb0t
yesterday
$begingroup$
I might ask a follow-up question about it in the weekend o_0 But change tracking is not that trivial /o
$endgroup$
– dfhwze
yesterday
$begingroup$
@dfhwze I'm not so sure we need all this tracking and undo logic. This demo doesn't show that but usually when you modify an immutable, you pass it so some deeper scope. When it returns, you pick up the previous version for this scope and continue from here. At leat this is how I mostly use immutables. They are convenient to update and use in a new context for a moment. Although it might be useful for debugging.
$endgroup$
– t3chb0t
yesterday
1
$begingroup$
@dfhwze oh, now I get it. then please do. I could use one of those too. So we'll be borrowing from each other haha
$endgroup$
– t3chb0t
yesterday
|
show 1 more comment
1
$begingroup$
This update/commit pattern makes me think we can tune this up to be used as a batch updater with support for multi-level change tracking/reverting implementing IEditableObject, IChangeTracking and IRevertibleChangeTracking (up-vote pending btw)
$endgroup$
– dfhwze
yesterday
$begingroup$
@dfhwze haha don't even think of it. Two more refactoring rounds and it can fly to the moon ;-P
$endgroup$
– t3chb0t
yesterday
$begingroup$
I might ask a follow-up question about it in the weekend o_0 But change tracking is not that trivial /o
$endgroup$
– dfhwze
yesterday
$begingroup$
@dfhwze I'm not so sure we need all this tracking and undo logic. This demo doesn't show that but usually when you modify an immutable, you pass it so some deeper scope. When it returns, you pick up the previous version for this scope and continue from here. At leat this is how I mostly use immutables. They are convenient to update and use in a new context for a moment. Although it might be useful for debugging.
$endgroup$
– t3chb0t
yesterday
1
$begingroup$
@dfhwze oh, now I get it. then please do. I could use one of those too. So we'll be borrowing from each other haha
$endgroup$
– t3chb0t
yesterday
1
1
$begingroup$
This update/commit pattern makes me think we can tune this up to be used as a batch updater with support for multi-level change tracking/reverting implementing IEditableObject, IChangeTracking and IRevertibleChangeTracking (up-vote pending btw)
$endgroup$
– dfhwze
yesterday
$begingroup$
This update/commit pattern makes me think we can tune this up to be used as a batch updater with support for multi-level change tracking/reverting implementing IEditableObject, IChangeTracking and IRevertibleChangeTracking (up-vote pending btw)
$endgroup$
– dfhwze
yesterday
$begingroup$
@dfhwze haha don't even think of it. Two more refactoring rounds and it can fly to the moon ;-P
$endgroup$
– t3chb0t
yesterday
$begingroup$
@dfhwze haha don't even think of it. Two more refactoring rounds and it can fly to the moon ;-P
$endgroup$
– t3chb0t
yesterday
$begingroup$
I might ask a follow-up question about it in the weekend o_0 But change tracking is not that trivial /o
$endgroup$
– dfhwze
yesterday
$begingroup$
I might ask a follow-up question about it in the weekend o_0 But change tracking is not that trivial /o
$endgroup$
– dfhwze
yesterday
$begingroup$
@dfhwze I'm not so sure we need all this tracking and undo logic. This demo doesn't show that but usually when you modify an immutable, you pass it so some deeper scope. When it returns, you pick up the previous version for this scope and continue from here. At leat this is how I mostly use immutables. They are convenient to update and use in a new context for a moment. Although it might be useful for debugging.
$endgroup$
– t3chb0t
yesterday
$begingroup$
@dfhwze I'm not so sure we need all this tracking and undo logic. This demo doesn't show that but usually when you modify an immutable, you pass it so some deeper scope. When it returns, you pick up the previous version for this scope and continue from here. At leat this is how I mostly use immutables. They are convenient to update and use in a new context for a moment. Although it might be useful for debugging.
$endgroup$
– t3chb0t
yesterday
1
1
$begingroup$
@dfhwze oh, now I get it. then please do. I could use one of those too. So we'll be borrowing from each other haha
$endgroup$
– t3chb0t
yesterday
$begingroup$
@dfhwze oh, now I get it. then please do. I could use one of those too. So we'll be borrowing from each other haha
$endgroup$
– t3chb0t
yesterday
|
show 1 more comment
Thanks for contributing an answer to Code Review Stack Exchange!
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
Use MathJax to format equations. MathJax reference.
To learn more, see our tips on writing great answers.
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f226694%2fimmutable-builder-and-updater%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
2
$begingroup$
This has a use case for building controllers and what not that have dozens of parameters. Does this also work with optional ctr arguments?
$endgroup$
– dfhwze
yesterday
1
$begingroup$
@dfhwze it doesn't... but it definitelly could... and should.
$endgroup$
– t3chb0t
yesterday
$begingroup$
Ok ... so let's improve it! :p
$endgroup$
– dfhwze
yesterday
$begingroup$
@dfhwze done! ;-] at least the main builder can do this now.
$endgroup$
– t3chb0t
yesterday
1
$begingroup$
I like the
With
extensions method that helps to create modified copies of mutable types. However I didn't get the use case of theImmutableBuilder
.. Why not just calling the constructor?$endgroup$
– JanDotNet
yesterday