Immutable type updater using a special constructor
I wanted to update some properties of my UriString but since this type is immutable, it wouldn't work. Actually, I just wanted to update the Path
property but I don't want to have a constructor with several parameters like in the other question because this is too much work and sometime not desireable. Instead, I require the immutable type to have an immutable-update-construtor that takes the ImmutableUpdate
object that I use for this purpose.
To the user it looks like a dummy class without any useful properties:
public sealed class ImmutableUpdate : IEnumerable<(PropertyInfo Property, object Value)>
{
private readonly IEnumerable<(PropertyInfo Property, object Value)> _updates;
internal ImmutableUpdate(IEnumerable<(PropertyInfo Property, object Value)> updates)
{
_updates = updates;
}
public IEnumerator<(PropertyInfo Property, object Value)> GetEnumerator() => _updates.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => _updates.GetEnumerator();
}
and it should, because it's only a carrier type for updates and an alias for the lengthy enumeration signature. These updates are created and used by the ImmutableUpdater
that binds values to properties via their backing-fields and allows the Bind
method to be called only from within the constructor of the object being updated. This should be a simple protection against mutating random instances. ImmutableUpdate
has also an internal
constructor and is sealed
which also prevents using it in a wrong way. (ImmutableUpdater
currently looks for properties that have no setter but extending it to understand some special attributes to customize this procees should be possible.)
public static class ImmutableUpdater
{
public static T With<T, TMember>(this T obj, Expression<Func<T, TMember>> memberSelector, TMember newValue)
{
if (!(memberSelector.Body is MemberExpression memberExpression))
{
throw new ArgumentException($"You must select a member. Affected expression '{memberSelector}'.");
}
if (!(memberExpression.Member is PropertyInfo selectedProperty))
{
throw new ArgumentException($"You must select a property. Affected expression '{memberSelector}'.");
}
if (selectedProperty.GetSetMethod() != null)
{
throw new ArgumentException($"You must select a readonly property. Affected expression '{memberSelector}'.");
}
if (GetBackingField<T>(selectedProperty.Name) == null)
{
throw new ArgumentException($"You must select a pure readonly property (not a computed one). Affected expression '{memberSelector}'.");
}
var immmutableUpdateCtor =
typeof(T)
.GetConstructor(new { typeof(ImmutableUpdate) });
var updates =
from property in obj.ImmutableProperties()
let getsUpdated = property.Name == selectedProperty.Name
select
(
property,
getsUpdated ? newValue : property.GetValue(obj)
);
return (T)Activator.CreateInstance(typeof(T), new ImmutableUpdate(updates));
}
public static void Bind<T>(this ImmutableUpdate update, T obj)
{
// todo - this could be cached
var isCalledByImmutableUpdateCtor = new StackFrame(1).GetMethod() == ImmutableUpdateConstructor(typeof(T));
if (!isCalledByImmutableUpdateCtor)
{
throw new InvalidOperationException($"You can call '{nameof(Bind)}' only from within an ImmutableUpdate constructor.");
}
foreach (var (property, value) in update)
{
GetBackingField<T>(property.Name)?.SetValue(obj, value);
}
}
private static FieldInfo GetBackingField<T>(string propertyName)
{
var backingFieldBindingFlags = BindingFlags.NonPublic | BindingFlags.Instance;
var backingFieldName = $"<{propertyName}>k__BackingField";
return typeof(T).GetField(backingFieldName, backingFieldBindingFlags);
}
private static IEnumerable<PropertyInfo> ImmutableProperties<T>(this T obj)
{
return
typeof(T)
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(p => p.GetSetMethod() is null);
}
private static ConstructorInfo ImmutableUpdateConstructor(Type type)
{
return type.GetConstructor(new { typeof(ImmutableUpdate) });
}
}
Example
Its usage is pretty simple, just use With
to set a new value.
var user = new User();
var updatedUser = user
.With(x => x.FirstName, "John")
.With(x => x.LastName, "Doe")
//.With(x => x.FullName, "Doe") // Boom!
.Dump();
user.Dump();
This is the type using Bind
inside its special constructor:
class User
{
public User() { }
public User(ImmutableUpdate update)
{
update.Bind(this);
}
public string FirstName { get; }
public string LastName { get; }
}
Is this solution any better than others, or worse? What do you say? I'm not really concerned about performance as this won't be used for any crazy scenarios (yet).
c# reflection extension-methods immutability
add a comment |
I wanted to update some properties of my UriString but since this type is immutable, it wouldn't work. Actually, I just wanted to update the Path
property but I don't want to have a constructor with several parameters like in the other question because this is too much work and sometime not desireable. Instead, I require the immutable type to have an immutable-update-construtor that takes the ImmutableUpdate
object that I use for this purpose.
To the user it looks like a dummy class without any useful properties:
public sealed class ImmutableUpdate : IEnumerable<(PropertyInfo Property, object Value)>
{
private readonly IEnumerable<(PropertyInfo Property, object Value)> _updates;
internal ImmutableUpdate(IEnumerable<(PropertyInfo Property, object Value)> updates)
{
_updates = updates;
}
public IEnumerator<(PropertyInfo Property, object Value)> GetEnumerator() => _updates.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => _updates.GetEnumerator();
}
and it should, because it's only a carrier type for updates and an alias for the lengthy enumeration signature. These updates are created and used by the ImmutableUpdater
that binds values to properties via their backing-fields and allows the Bind
method to be called only from within the constructor of the object being updated. This should be a simple protection against mutating random instances. ImmutableUpdate
has also an internal
constructor and is sealed
which also prevents using it in a wrong way. (ImmutableUpdater
currently looks for properties that have no setter but extending it to understand some special attributes to customize this procees should be possible.)
public static class ImmutableUpdater
{
public static T With<T, TMember>(this T obj, Expression<Func<T, TMember>> memberSelector, TMember newValue)
{
if (!(memberSelector.Body is MemberExpression memberExpression))
{
throw new ArgumentException($"You must select a member. Affected expression '{memberSelector}'.");
}
if (!(memberExpression.Member is PropertyInfo selectedProperty))
{
throw new ArgumentException($"You must select a property. Affected expression '{memberSelector}'.");
}
if (selectedProperty.GetSetMethod() != null)
{
throw new ArgumentException($"You must select a readonly property. Affected expression '{memberSelector}'.");
}
if (GetBackingField<T>(selectedProperty.Name) == null)
{
throw new ArgumentException($"You must select a pure readonly property (not a computed one). Affected expression '{memberSelector}'.");
}
var immmutableUpdateCtor =
typeof(T)
.GetConstructor(new { typeof(ImmutableUpdate) });
var updates =
from property in obj.ImmutableProperties()
let getsUpdated = property.Name == selectedProperty.Name
select
(
property,
getsUpdated ? newValue : property.GetValue(obj)
);
return (T)Activator.CreateInstance(typeof(T), new ImmutableUpdate(updates));
}
public static void Bind<T>(this ImmutableUpdate update, T obj)
{
// todo - this could be cached
var isCalledByImmutableUpdateCtor = new StackFrame(1).GetMethod() == ImmutableUpdateConstructor(typeof(T));
if (!isCalledByImmutableUpdateCtor)
{
throw new InvalidOperationException($"You can call '{nameof(Bind)}' only from within an ImmutableUpdate constructor.");
}
foreach (var (property, value) in update)
{
GetBackingField<T>(property.Name)?.SetValue(obj, value);
}
}
private static FieldInfo GetBackingField<T>(string propertyName)
{
var backingFieldBindingFlags = BindingFlags.NonPublic | BindingFlags.Instance;
var backingFieldName = $"<{propertyName}>k__BackingField";
return typeof(T).GetField(backingFieldName, backingFieldBindingFlags);
}
private static IEnumerable<PropertyInfo> ImmutableProperties<T>(this T obj)
{
return
typeof(T)
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(p => p.GetSetMethod() is null);
}
private static ConstructorInfo ImmutableUpdateConstructor(Type type)
{
return type.GetConstructor(new { typeof(ImmutableUpdate) });
}
}
Example
Its usage is pretty simple, just use With
to set a new value.
var user = new User();
var updatedUser = user
.With(x => x.FirstName, "John")
.With(x => x.LastName, "Doe")
//.With(x => x.FullName, "Doe") // Boom!
.Dump();
user.Dump();
This is the type using Bind
inside its special constructor:
class User
{
public User() { }
public User(ImmutableUpdate update)
{
update.Bind(this);
}
public string FirstName { get; }
public string LastName { get; }
}
Is this solution any better than others, or worse? What do you say? I'm not really concerned about performance as this won't be used for any crazy scenarios (yet).
c# reflection extension-methods immutability
1
I was literally just contemplating writing one of these... I was wondering how best to specify properties, and had completely forgotten about Expressions, so thanks ;)
– VisualMelon
Dec 20 at 15:34
1
At first blush, the chainedWith
s that return a new object each time seem a little wasteful if the next one is discarding the previous. Perhaps it could return an "interim" container object that can accumulate theWith
expressions and a finalApply()
that executes them once and produces a singleUser
(in this case) object?
– Jesse C. Slicer
Dec 20 at 17:16
add a comment |
I wanted to update some properties of my UriString but since this type is immutable, it wouldn't work. Actually, I just wanted to update the Path
property but I don't want to have a constructor with several parameters like in the other question because this is too much work and sometime not desireable. Instead, I require the immutable type to have an immutable-update-construtor that takes the ImmutableUpdate
object that I use for this purpose.
To the user it looks like a dummy class without any useful properties:
public sealed class ImmutableUpdate : IEnumerable<(PropertyInfo Property, object Value)>
{
private readonly IEnumerable<(PropertyInfo Property, object Value)> _updates;
internal ImmutableUpdate(IEnumerable<(PropertyInfo Property, object Value)> updates)
{
_updates = updates;
}
public IEnumerator<(PropertyInfo Property, object Value)> GetEnumerator() => _updates.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => _updates.GetEnumerator();
}
and it should, because it's only a carrier type for updates and an alias for the lengthy enumeration signature. These updates are created and used by the ImmutableUpdater
that binds values to properties via their backing-fields and allows the Bind
method to be called only from within the constructor of the object being updated. This should be a simple protection against mutating random instances. ImmutableUpdate
has also an internal
constructor and is sealed
which also prevents using it in a wrong way. (ImmutableUpdater
currently looks for properties that have no setter but extending it to understand some special attributes to customize this procees should be possible.)
public static class ImmutableUpdater
{
public static T With<T, TMember>(this T obj, Expression<Func<T, TMember>> memberSelector, TMember newValue)
{
if (!(memberSelector.Body is MemberExpression memberExpression))
{
throw new ArgumentException($"You must select a member. Affected expression '{memberSelector}'.");
}
if (!(memberExpression.Member is PropertyInfo selectedProperty))
{
throw new ArgumentException($"You must select a property. Affected expression '{memberSelector}'.");
}
if (selectedProperty.GetSetMethod() != null)
{
throw new ArgumentException($"You must select a readonly property. Affected expression '{memberSelector}'.");
}
if (GetBackingField<T>(selectedProperty.Name) == null)
{
throw new ArgumentException($"You must select a pure readonly property (not a computed one). Affected expression '{memberSelector}'.");
}
var immmutableUpdateCtor =
typeof(T)
.GetConstructor(new { typeof(ImmutableUpdate) });
var updates =
from property in obj.ImmutableProperties()
let getsUpdated = property.Name == selectedProperty.Name
select
(
property,
getsUpdated ? newValue : property.GetValue(obj)
);
return (T)Activator.CreateInstance(typeof(T), new ImmutableUpdate(updates));
}
public static void Bind<T>(this ImmutableUpdate update, T obj)
{
// todo - this could be cached
var isCalledByImmutableUpdateCtor = new StackFrame(1).GetMethod() == ImmutableUpdateConstructor(typeof(T));
if (!isCalledByImmutableUpdateCtor)
{
throw new InvalidOperationException($"You can call '{nameof(Bind)}' only from within an ImmutableUpdate constructor.");
}
foreach (var (property, value) in update)
{
GetBackingField<T>(property.Name)?.SetValue(obj, value);
}
}
private static FieldInfo GetBackingField<T>(string propertyName)
{
var backingFieldBindingFlags = BindingFlags.NonPublic | BindingFlags.Instance;
var backingFieldName = $"<{propertyName}>k__BackingField";
return typeof(T).GetField(backingFieldName, backingFieldBindingFlags);
}
private static IEnumerable<PropertyInfo> ImmutableProperties<T>(this T obj)
{
return
typeof(T)
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(p => p.GetSetMethod() is null);
}
private static ConstructorInfo ImmutableUpdateConstructor(Type type)
{
return type.GetConstructor(new { typeof(ImmutableUpdate) });
}
}
Example
Its usage is pretty simple, just use With
to set a new value.
var user = new User();
var updatedUser = user
.With(x => x.FirstName, "John")
.With(x => x.LastName, "Doe")
//.With(x => x.FullName, "Doe") // Boom!
.Dump();
user.Dump();
This is the type using Bind
inside its special constructor:
class User
{
public User() { }
public User(ImmutableUpdate update)
{
update.Bind(this);
}
public string FirstName { get; }
public string LastName { get; }
}
Is this solution any better than others, or worse? What do you say? I'm not really concerned about performance as this won't be used for any crazy scenarios (yet).
c# reflection extension-methods immutability
I wanted to update some properties of my UriString but since this type is immutable, it wouldn't work. Actually, I just wanted to update the Path
property but I don't want to have a constructor with several parameters like in the other question because this is too much work and sometime not desireable. Instead, I require the immutable type to have an immutable-update-construtor that takes the ImmutableUpdate
object that I use for this purpose.
To the user it looks like a dummy class without any useful properties:
public sealed class ImmutableUpdate : IEnumerable<(PropertyInfo Property, object Value)>
{
private readonly IEnumerable<(PropertyInfo Property, object Value)> _updates;
internal ImmutableUpdate(IEnumerable<(PropertyInfo Property, object Value)> updates)
{
_updates = updates;
}
public IEnumerator<(PropertyInfo Property, object Value)> GetEnumerator() => _updates.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => _updates.GetEnumerator();
}
and it should, because it's only a carrier type for updates and an alias for the lengthy enumeration signature. These updates are created and used by the ImmutableUpdater
that binds values to properties via their backing-fields and allows the Bind
method to be called only from within the constructor of the object being updated. This should be a simple protection against mutating random instances. ImmutableUpdate
has also an internal
constructor and is sealed
which also prevents using it in a wrong way. (ImmutableUpdater
currently looks for properties that have no setter but extending it to understand some special attributes to customize this procees should be possible.)
public static class ImmutableUpdater
{
public static T With<T, TMember>(this T obj, Expression<Func<T, TMember>> memberSelector, TMember newValue)
{
if (!(memberSelector.Body is MemberExpression memberExpression))
{
throw new ArgumentException($"You must select a member. Affected expression '{memberSelector}'.");
}
if (!(memberExpression.Member is PropertyInfo selectedProperty))
{
throw new ArgumentException($"You must select a property. Affected expression '{memberSelector}'.");
}
if (selectedProperty.GetSetMethod() != null)
{
throw new ArgumentException($"You must select a readonly property. Affected expression '{memberSelector}'.");
}
if (GetBackingField<T>(selectedProperty.Name) == null)
{
throw new ArgumentException($"You must select a pure readonly property (not a computed one). Affected expression '{memberSelector}'.");
}
var immmutableUpdateCtor =
typeof(T)
.GetConstructor(new { typeof(ImmutableUpdate) });
var updates =
from property in obj.ImmutableProperties()
let getsUpdated = property.Name == selectedProperty.Name
select
(
property,
getsUpdated ? newValue : property.GetValue(obj)
);
return (T)Activator.CreateInstance(typeof(T), new ImmutableUpdate(updates));
}
public static void Bind<T>(this ImmutableUpdate update, T obj)
{
// todo - this could be cached
var isCalledByImmutableUpdateCtor = new StackFrame(1).GetMethod() == ImmutableUpdateConstructor(typeof(T));
if (!isCalledByImmutableUpdateCtor)
{
throw new InvalidOperationException($"You can call '{nameof(Bind)}' only from within an ImmutableUpdate constructor.");
}
foreach (var (property, value) in update)
{
GetBackingField<T>(property.Name)?.SetValue(obj, value);
}
}
private static FieldInfo GetBackingField<T>(string propertyName)
{
var backingFieldBindingFlags = BindingFlags.NonPublic | BindingFlags.Instance;
var backingFieldName = $"<{propertyName}>k__BackingField";
return typeof(T).GetField(backingFieldName, backingFieldBindingFlags);
}
private static IEnumerable<PropertyInfo> ImmutableProperties<T>(this T obj)
{
return
typeof(T)
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(p => p.GetSetMethod() is null);
}
private static ConstructorInfo ImmutableUpdateConstructor(Type type)
{
return type.GetConstructor(new { typeof(ImmutableUpdate) });
}
}
Example
Its usage is pretty simple, just use With
to set a new value.
var user = new User();
var updatedUser = user
.With(x => x.FirstName, "John")
.With(x => x.LastName, "Doe")
//.With(x => x.FullName, "Doe") // Boom!
.Dump();
user.Dump();
This is the type using Bind
inside its special constructor:
class User
{
public User() { }
public User(ImmutableUpdate update)
{
update.Bind(this);
}
public string FirstName { get; }
public string LastName { get; }
}
Is this solution any better than others, or worse? What do you say? I'm not really concerned about performance as this won't be used for any crazy scenarios (yet).
c# reflection extension-methods immutability
c# reflection extension-methods immutability
edited Dec 20 at 15:44
Heslacher
44.8k460155
44.8k460155
asked Dec 20 at 15:31
t3chb0t
34k746112
34k746112
1
I was literally just contemplating writing one of these... I was wondering how best to specify properties, and had completely forgotten about Expressions, so thanks ;)
– VisualMelon
Dec 20 at 15:34
1
At first blush, the chainedWith
s that return a new object each time seem a little wasteful if the next one is discarding the previous. Perhaps it could return an "interim" container object that can accumulate theWith
expressions and a finalApply()
that executes them once and produces a singleUser
(in this case) object?
– Jesse C. Slicer
Dec 20 at 17:16
add a comment |
1
I was literally just contemplating writing one of these... I was wondering how best to specify properties, and had completely forgotten about Expressions, so thanks ;)
– VisualMelon
Dec 20 at 15:34
1
At first blush, the chainedWith
s that return a new object each time seem a little wasteful if the next one is discarding the previous. Perhaps it could return an "interim" container object that can accumulate theWith
expressions and a finalApply()
that executes them once and produces a singleUser
(in this case) object?
– Jesse C. Slicer
Dec 20 at 17:16
1
1
I was literally just contemplating writing one of these... I was wondering how best to specify properties, and had completely forgotten about Expressions, so thanks ;)
– VisualMelon
Dec 20 at 15:34
I was literally just contemplating writing one of these... I was wondering how best to specify properties, and had completely forgotten about Expressions, so thanks ;)
– VisualMelon
Dec 20 at 15:34
1
1
At first blush, the chained
With
s that return a new object each time seem a little wasteful if the next one is discarding the previous. Perhaps it could return an "interim" container object that can accumulate the With
expressions and a final Apply()
that executes them once and produces a single User
(in this case) object?– Jesse C. Slicer
Dec 20 at 17:16
At first blush, the chained
With
s that return a new object each time seem a little wasteful if the next one is discarding the previous. Perhaps it could return an "interim" container object that can accumulate the With
expressions and a final Apply()
that executes them once and produces a single User
(in this case) object?– Jesse C. Slicer
Dec 20 at 17:16
add a comment |
1 Answer
1
active
oldest
votes
A couple of small things here. My bigger thought is in the comment on the original question.
One:
Empty collections are nice if null
s show their ugly faces:
internal ImmutableUpdate(IEnumerable<(PropertyInfo Property, object Value)> updates)
{
_updates = updates ?? Enumerable.Empty<(PropertyInfo Property, object Value)>();
}
Two:
var immmutableUpdateCtor =
typeof(T)
.GetConstructor(new { typeof(ImmutableUpdate) });
is never used. I'd rewrite that little block as:
var immmutableUpdateCtor = ImmutableUpdateConstructor(typeof(T));
if (immutableUpdateCtor == null)
{
return obj;
}
var updates =
from property in obj.ImmutableProperties()
let getsUpdated = property.Name == selectedProperty.Name
select
(
property,
getsUpdated ? newValue : property.GetValue(obj)
);
return (T)immutableUpdateCtor.Invoke(new object { new ImmutableUpdate(updates) });
I'll addend more if I think of anything.
Three:
The caching of constructor information as was commented in the code:
private static readonly ConcurrentDictionary<Type, ConstructorInfo> _ImmutableConstructors =
new ConcurrentDictionary<Type, ConstructorInfo>();
private static ConstructorInfo ImmutableUpdateConstructor(Type type)
{
if (!_ImmutableConstructors.TryGetValue(type, out var constructor))
{
constructor = type.GetConstructor(new { typeof(ImmutableUpdate) });
_ImmutableConstructors.TryAdd(type, constructor);
}
return constructor;
}
Four:
Here are the builder pieces:
In ImmutableUpdater
class:
public static UpdateBuilder<T> With<T, TMember>(this T obj, Expression<Func<T, TMember>> memberSelector, TMember newValue)
{
ConstructorInfo immutableUpdateCtor = ImmutableUpdateConstructor(typeof(T));
return new UpdateBuilder<T>(obj, immutableUpdateCtor).With(memberSelector, newValue);
}
public static UpdateBuilder<T> With<T, TMember>(this UpdateBuilder<T> obj, Expression<Func<T, TMember>> memberSelector, TMember newValue)
{
if (!(memberSelector.Body is MemberExpression memberExpression))
{
throw new ArgumentException($"You must select a member. Affected expression '{memberSelector}'.");
}
if (!(memberExpression.Member is PropertyInfo selectedProperty))
{
throw new ArgumentException($"You must select a property. Affected expression '{memberSelector}'.");
}
if (selectedProperty.GetSetMethod() != null)
{
throw new ArgumentException(
$"You must select a readonly property. Affected expression '{memberSelector}'.");
}
if (selectedProperty.Name.GetBackingField<T>() == null)
{
throw new ArgumentException(
$"You must select a pure readonly property (not a computed one). Affected expression '{memberSelector}'.");
}
var updates =
from property in obj.ImmutableProperties()
where property.Name == selectedProperty.Name
select
(
property, (object)newValue
);
return obj.Add(updates);
}
private static IEnumerable<PropertyInfo> ImmutableProperties<T>(this UpdateBuilder<T> obj)
{
return
typeof(T)
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(propertyInfo => propertyInfo.GetSetMethod() is null);
}
and finally, the UpdateBuilder<T>
class:
using System.Linq;
using System.Reflection;
using IEnumerablePropertyValue = System.Collections.Generic.IEnumerable<(System.Reflection.PropertyInfo Property, object Value)>;
using PropertyValueList = System.Collections.Generic.List<(System.Reflection.PropertyInfo Property, object Value)>;
public sealed class UpdateBuilder<T>
{
private readonly PropertyValueList _updates = new PropertyValueList();
private readonly ConstructorInfo _immutableUpdateCtor;
public UpdateBuilder(T obj, ConstructorInfo immutableUpdateCtor)
{
this.Object = obj;
this._immutableUpdateCtor = immutableUpdateCtor;
}
public T Object { get; }
public UpdateBuilder<T> Add(IEnumerablePropertyValue updates)
{
foreach (var update in updates ?? Enumerable.Empty<(PropertyInfo Property, object Value)>())
{
this._updates.Add(update);
}
return this;
}
public static implicit operator T(UpdateBuilder<T> updateBuilder)
{
if (updateBuilder == null)
{
return default(T);
}
if (updateBuilder._immutableUpdateCtor == null)
{
return updateBuilder.Object;
}
return (T)updateBuilder._immutableUpdateCtor.Invoke(new object { new ImmutableUpdate(updateBuilder._updates) });
}
1
Ooops, it stayed there after moving it do the helper ;-]
– t3chb0t
Dec 20 at 17:35
1
You must like it ;-) I see you've added a builder. I'll definitely borrow it.
– t3chb0t
Dec 20 at 20:21
add a comment |
Your Answer
StackExchange.ifUsing("editor", function () {
return StackExchange.using("mathjaxEditing", function () {
StackExchange.MarkdownEditor.creationCallbacks.add(function (editor, postfix) {
StackExchange.mathjaxEditing.prepareWmdForMathJax(editor, postfix, [["\$", "\$"]]);
});
});
}, "mathjax-editing");
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%2f210056%2fimmutable-type-updater-using-a-special-constructor%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
1 Answer
1
active
oldest
votes
1 Answer
1
active
oldest
votes
active
oldest
votes
active
oldest
votes
A couple of small things here. My bigger thought is in the comment on the original question.
One:
Empty collections are nice if null
s show their ugly faces:
internal ImmutableUpdate(IEnumerable<(PropertyInfo Property, object Value)> updates)
{
_updates = updates ?? Enumerable.Empty<(PropertyInfo Property, object Value)>();
}
Two:
var immmutableUpdateCtor =
typeof(T)
.GetConstructor(new { typeof(ImmutableUpdate) });
is never used. I'd rewrite that little block as:
var immmutableUpdateCtor = ImmutableUpdateConstructor(typeof(T));
if (immutableUpdateCtor == null)
{
return obj;
}
var updates =
from property in obj.ImmutableProperties()
let getsUpdated = property.Name == selectedProperty.Name
select
(
property,
getsUpdated ? newValue : property.GetValue(obj)
);
return (T)immutableUpdateCtor.Invoke(new object { new ImmutableUpdate(updates) });
I'll addend more if I think of anything.
Three:
The caching of constructor information as was commented in the code:
private static readonly ConcurrentDictionary<Type, ConstructorInfo> _ImmutableConstructors =
new ConcurrentDictionary<Type, ConstructorInfo>();
private static ConstructorInfo ImmutableUpdateConstructor(Type type)
{
if (!_ImmutableConstructors.TryGetValue(type, out var constructor))
{
constructor = type.GetConstructor(new { typeof(ImmutableUpdate) });
_ImmutableConstructors.TryAdd(type, constructor);
}
return constructor;
}
Four:
Here are the builder pieces:
In ImmutableUpdater
class:
public static UpdateBuilder<T> With<T, TMember>(this T obj, Expression<Func<T, TMember>> memberSelector, TMember newValue)
{
ConstructorInfo immutableUpdateCtor = ImmutableUpdateConstructor(typeof(T));
return new UpdateBuilder<T>(obj, immutableUpdateCtor).With(memberSelector, newValue);
}
public static UpdateBuilder<T> With<T, TMember>(this UpdateBuilder<T> obj, Expression<Func<T, TMember>> memberSelector, TMember newValue)
{
if (!(memberSelector.Body is MemberExpression memberExpression))
{
throw new ArgumentException($"You must select a member. Affected expression '{memberSelector}'.");
}
if (!(memberExpression.Member is PropertyInfo selectedProperty))
{
throw new ArgumentException($"You must select a property. Affected expression '{memberSelector}'.");
}
if (selectedProperty.GetSetMethod() != null)
{
throw new ArgumentException(
$"You must select a readonly property. Affected expression '{memberSelector}'.");
}
if (selectedProperty.Name.GetBackingField<T>() == null)
{
throw new ArgumentException(
$"You must select a pure readonly property (not a computed one). Affected expression '{memberSelector}'.");
}
var updates =
from property in obj.ImmutableProperties()
where property.Name == selectedProperty.Name
select
(
property, (object)newValue
);
return obj.Add(updates);
}
private static IEnumerable<PropertyInfo> ImmutableProperties<T>(this UpdateBuilder<T> obj)
{
return
typeof(T)
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(propertyInfo => propertyInfo.GetSetMethod() is null);
}
and finally, the UpdateBuilder<T>
class:
using System.Linq;
using System.Reflection;
using IEnumerablePropertyValue = System.Collections.Generic.IEnumerable<(System.Reflection.PropertyInfo Property, object Value)>;
using PropertyValueList = System.Collections.Generic.List<(System.Reflection.PropertyInfo Property, object Value)>;
public sealed class UpdateBuilder<T>
{
private readonly PropertyValueList _updates = new PropertyValueList();
private readonly ConstructorInfo _immutableUpdateCtor;
public UpdateBuilder(T obj, ConstructorInfo immutableUpdateCtor)
{
this.Object = obj;
this._immutableUpdateCtor = immutableUpdateCtor;
}
public T Object { get; }
public UpdateBuilder<T> Add(IEnumerablePropertyValue updates)
{
foreach (var update in updates ?? Enumerable.Empty<(PropertyInfo Property, object Value)>())
{
this._updates.Add(update);
}
return this;
}
public static implicit operator T(UpdateBuilder<T> updateBuilder)
{
if (updateBuilder == null)
{
return default(T);
}
if (updateBuilder._immutableUpdateCtor == null)
{
return updateBuilder.Object;
}
return (T)updateBuilder._immutableUpdateCtor.Invoke(new object { new ImmutableUpdate(updateBuilder._updates) });
}
1
Ooops, it stayed there after moving it do the helper ;-]
– t3chb0t
Dec 20 at 17:35
1
You must like it ;-) I see you've added a builder. I'll definitely borrow it.
– t3chb0t
Dec 20 at 20:21
add a comment |
A couple of small things here. My bigger thought is in the comment on the original question.
One:
Empty collections are nice if null
s show their ugly faces:
internal ImmutableUpdate(IEnumerable<(PropertyInfo Property, object Value)> updates)
{
_updates = updates ?? Enumerable.Empty<(PropertyInfo Property, object Value)>();
}
Two:
var immmutableUpdateCtor =
typeof(T)
.GetConstructor(new { typeof(ImmutableUpdate) });
is never used. I'd rewrite that little block as:
var immmutableUpdateCtor = ImmutableUpdateConstructor(typeof(T));
if (immutableUpdateCtor == null)
{
return obj;
}
var updates =
from property in obj.ImmutableProperties()
let getsUpdated = property.Name == selectedProperty.Name
select
(
property,
getsUpdated ? newValue : property.GetValue(obj)
);
return (T)immutableUpdateCtor.Invoke(new object { new ImmutableUpdate(updates) });
I'll addend more if I think of anything.
Three:
The caching of constructor information as was commented in the code:
private static readonly ConcurrentDictionary<Type, ConstructorInfo> _ImmutableConstructors =
new ConcurrentDictionary<Type, ConstructorInfo>();
private static ConstructorInfo ImmutableUpdateConstructor(Type type)
{
if (!_ImmutableConstructors.TryGetValue(type, out var constructor))
{
constructor = type.GetConstructor(new { typeof(ImmutableUpdate) });
_ImmutableConstructors.TryAdd(type, constructor);
}
return constructor;
}
Four:
Here are the builder pieces:
In ImmutableUpdater
class:
public static UpdateBuilder<T> With<T, TMember>(this T obj, Expression<Func<T, TMember>> memberSelector, TMember newValue)
{
ConstructorInfo immutableUpdateCtor = ImmutableUpdateConstructor(typeof(T));
return new UpdateBuilder<T>(obj, immutableUpdateCtor).With(memberSelector, newValue);
}
public static UpdateBuilder<T> With<T, TMember>(this UpdateBuilder<T> obj, Expression<Func<T, TMember>> memberSelector, TMember newValue)
{
if (!(memberSelector.Body is MemberExpression memberExpression))
{
throw new ArgumentException($"You must select a member. Affected expression '{memberSelector}'.");
}
if (!(memberExpression.Member is PropertyInfo selectedProperty))
{
throw new ArgumentException($"You must select a property. Affected expression '{memberSelector}'.");
}
if (selectedProperty.GetSetMethod() != null)
{
throw new ArgumentException(
$"You must select a readonly property. Affected expression '{memberSelector}'.");
}
if (selectedProperty.Name.GetBackingField<T>() == null)
{
throw new ArgumentException(
$"You must select a pure readonly property (not a computed one). Affected expression '{memberSelector}'.");
}
var updates =
from property in obj.ImmutableProperties()
where property.Name == selectedProperty.Name
select
(
property, (object)newValue
);
return obj.Add(updates);
}
private static IEnumerable<PropertyInfo> ImmutableProperties<T>(this UpdateBuilder<T> obj)
{
return
typeof(T)
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(propertyInfo => propertyInfo.GetSetMethod() is null);
}
and finally, the UpdateBuilder<T>
class:
using System.Linq;
using System.Reflection;
using IEnumerablePropertyValue = System.Collections.Generic.IEnumerable<(System.Reflection.PropertyInfo Property, object Value)>;
using PropertyValueList = System.Collections.Generic.List<(System.Reflection.PropertyInfo Property, object Value)>;
public sealed class UpdateBuilder<T>
{
private readonly PropertyValueList _updates = new PropertyValueList();
private readonly ConstructorInfo _immutableUpdateCtor;
public UpdateBuilder(T obj, ConstructorInfo immutableUpdateCtor)
{
this.Object = obj;
this._immutableUpdateCtor = immutableUpdateCtor;
}
public T Object { get; }
public UpdateBuilder<T> Add(IEnumerablePropertyValue updates)
{
foreach (var update in updates ?? Enumerable.Empty<(PropertyInfo Property, object Value)>())
{
this._updates.Add(update);
}
return this;
}
public static implicit operator T(UpdateBuilder<T> updateBuilder)
{
if (updateBuilder == null)
{
return default(T);
}
if (updateBuilder._immutableUpdateCtor == null)
{
return updateBuilder.Object;
}
return (T)updateBuilder._immutableUpdateCtor.Invoke(new object { new ImmutableUpdate(updateBuilder._updates) });
}
1
Ooops, it stayed there after moving it do the helper ;-]
– t3chb0t
Dec 20 at 17:35
1
You must like it ;-) I see you've added a builder. I'll definitely borrow it.
– t3chb0t
Dec 20 at 20:21
add a comment |
A couple of small things here. My bigger thought is in the comment on the original question.
One:
Empty collections are nice if null
s show their ugly faces:
internal ImmutableUpdate(IEnumerable<(PropertyInfo Property, object Value)> updates)
{
_updates = updates ?? Enumerable.Empty<(PropertyInfo Property, object Value)>();
}
Two:
var immmutableUpdateCtor =
typeof(T)
.GetConstructor(new { typeof(ImmutableUpdate) });
is never used. I'd rewrite that little block as:
var immmutableUpdateCtor = ImmutableUpdateConstructor(typeof(T));
if (immutableUpdateCtor == null)
{
return obj;
}
var updates =
from property in obj.ImmutableProperties()
let getsUpdated = property.Name == selectedProperty.Name
select
(
property,
getsUpdated ? newValue : property.GetValue(obj)
);
return (T)immutableUpdateCtor.Invoke(new object { new ImmutableUpdate(updates) });
I'll addend more if I think of anything.
Three:
The caching of constructor information as was commented in the code:
private static readonly ConcurrentDictionary<Type, ConstructorInfo> _ImmutableConstructors =
new ConcurrentDictionary<Type, ConstructorInfo>();
private static ConstructorInfo ImmutableUpdateConstructor(Type type)
{
if (!_ImmutableConstructors.TryGetValue(type, out var constructor))
{
constructor = type.GetConstructor(new { typeof(ImmutableUpdate) });
_ImmutableConstructors.TryAdd(type, constructor);
}
return constructor;
}
Four:
Here are the builder pieces:
In ImmutableUpdater
class:
public static UpdateBuilder<T> With<T, TMember>(this T obj, Expression<Func<T, TMember>> memberSelector, TMember newValue)
{
ConstructorInfo immutableUpdateCtor = ImmutableUpdateConstructor(typeof(T));
return new UpdateBuilder<T>(obj, immutableUpdateCtor).With(memberSelector, newValue);
}
public static UpdateBuilder<T> With<T, TMember>(this UpdateBuilder<T> obj, Expression<Func<T, TMember>> memberSelector, TMember newValue)
{
if (!(memberSelector.Body is MemberExpression memberExpression))
{
throw new ArgumentException($"You must select a member. Affected expression '{memberSelector}'.");
}
if (!(memberExpression.Member is PropertyInfo selectedProperty))
{
throw new ArgumentException($"You must select a property. Affected expression '{memberSelector}'.");
}
if (selectedProperty.GetSetMethod() != null)
{
throw new ArgumentException(
$"You must select a readonly property. Affected expression '{memberSelector}'.");
}
if (selectedProperty.Name.GetBackingField<T>() == null)
{
throw new ArgumentException(
$"You must select a pure readonly property (not a computed one). Affected expression '{memberSelector}'.");
}
var updates =
from property in obj.ImmutableProperties()
where property.Name == selectedProperty.Name
select
(
property, (object)newValue
);
return obj.Add(updates);
}
private static IEnumerable<PropertyInfo> ImmutableProperties<T>(this UpdateBuilder<T> obj)
{
return
typeof(T)
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(propertyInfo => propertyInfo.GetSetMethod() is null);
}
and finally, the UpdateBuilder<T>
class:
using System.Linq;
using System.Reflection;
using IEnumerablePropertyValue = System.Collections.Generic.IEnumerable<(System.Reflection.PropertyInfo Property, object Value)>;
using PropertyValueList = System.Collections.Generic.List<(System.Reflection.PropertyInfo Property, object Value)>;
public sealed class UpdateBuilder<T>
{
private readonly PropertyValueList _updates = new PropertyValueList();
private readonly ConstructorInfo _immutableUpdateCtor;
public UpdateBuilder(T obj, ConstructorInfo immutableUpdateCtor)
{
this.Object = obj;
this._immutableUpdateCtor = immutableUpdateCtor;
}
public T Object { get; }
public UpdateBuilder<T> Add(IEnumerablePropertyValue updates)
{
foreach (var update in updates ?? Enumerable.Empty<(PropertyInfo Property, object Value)>())
{
this._updates.Add(update);
}
return this;
}
public static implicit operator T(UpdateBuilder<T> updateBuilder)
{
if (updateBuilder == null)
{
return default(T);
}
if (updateBuilder._immutableUpdateCtor == null)
{
return updateBuilder.Object;
}
return (T)updateBuilder._immutableUpdateCtor.Invoke(new object { new ImmutableUpdate(updateBuilder._updates) });
}
A couple of small things here. My bigger thought is in the comment on the original question.
One:
Empty collections are nice if null
s show their ugly faces:
internal ImmutableUpdate(IEnumerable<(PropertyInfo Property, object Value)> updates)
{
_updates = updates ?? Enumerable.Empty<(PropertyInfo Property, object Value)>();
}
Two:
var immmutableUpdateCtor =
typeof(T)
.GetConstructor(new { typeof(ImmutableUpdate) });
is never used. I'd rewrite that little block as:
var immmutableUpdateCtor = ImmutableUpdateConstructor(typeof(T));
if (immutableUpdateCtor == null)
{
return obj;
}
var updates =
from property in obj.ImmutableProperties()
let getsUpdated = property.Name == selectedProperty.Name
select
(
property,
getsUpdated ? newValue : property.GetValue(obj)
);
return (T)immutableUpdateCtor.Invoke(new object { new ImmutableUpdate(updates) });
I'll addend more if I think of anything.
Three:
The caching of constructor information as was commented in the code:
private static readonly ConcurrentDictionary<Type, ConstructorInfo> _ImmutableConstructors =
new ConcurrentDictionary<Type, ConstructorInfo>();
private static ConstructorInfo ImmutableUpdateConstructor(Type type)
{
if (!_ImmutableConstructors.TryGetValue(type, out var constructor))
{
constructor = type.GetConstructor(new { typeof(ImmutableUpdate) });
_ImmutableConstructors.TryAdd(type, constructor);
}
return constructor;
}
Four:
Here are the builder pieces:
In ImmutableUpdater
class:
public static UpdateBuilder<T> With<T, TMember>(this T obj, Expression<Func<T, TMember>> memberSelector, TMember newValue)
{
ConstructorInfo immutableUpdateCtor = ImmutableUpdateConstructor(typeof(T));
return new UpdateBuilder<T>(obj, immutableUpdateCtor).With(memberSelector, newValue);
}
public static UpdateBuilder<T> With<T, TMember>(this UpdateBuilder<T> obj, Expression<Func<T, TMember>> memberSelector, TMember newValue)
{
if (!(memberSelector.Body is MemberExpression memberExpression))
{
throw new ArgumentException($"You must select a member. Affected expression '{memberSelector}'.");
}
if (!(memberExpression.Member is PropertyInfo selectedProperty))
{
throw new ArgumentException($"You must select a property. Affected expression '{memberSelector}'.");
}
if (selectedProperty.GetSetMethod() != null)
{
throw new ArgumentException(
$"You must select a readonly property. Affected expression '{memberSelector}'.");
}
if (selectedProperty.Name.GetBackingField<T>() == null)
{
throw new ArgumentException(
$"You must select a pure readonly property (not a computed one). Affected expression '{memberSelector}'.");
}
var updates =
from property in obj.ImmutableProperties()
where property.Name == selectedProperty.Name
select
(
property, (object)newValue
);
return obj.Add(updates);
}
private static IEnumerable<PropertyInfo> ImmutableProperties<T>(this UpdateBuilder<T> obj)
{
return
typeof(T)
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(propertyInfo => propertyInfo.GetSetMethod() is null);
}
and finally, the UpdateBuilder<T>
class:
using System.Linq;
using System.Reflection;
using IEnumerablePropertyValue = System.Collections.Generic.IEnumerable<(System.Reflection.PropertyInfo Property, object Value)>;
using PropertyValueList = System.Collections.Generic.List<(System.Reflection.PropertyInfo Property, object Value)>;
public sealed class UpdateBuilder<T>
{
private readonly PropertyValueList _updates = new PropertyValueList();
private readonly ConstructorInfo _immutableUpdateCtor;
public UpdateBuilder(T obj, ConstructorInfo immutableUpdateCtor)
{
this.Object = obj;
this._immutableUpdateCtor = immutableUpdateCtor;
}
public T Object { get; }
public UpdateBuilder<T> Add(IEnumerablePropertyValue updates)
{
foreach (var update in updates ?? Enumerable.Empty<(PropertyInfo Property, object Value)>())
{
this._updates.Add(update);
}
return this;
}
public static implicit operator T(UpdateBuilder<T> updateBuilder)
{
if (updateBuilder == null)
{
return default(T);
}
if (updateBuilder._immutableUpdateCtor == null)
{
return updateBuilder.Object;
}
return (T)updateBuilder._immutableUpdateCtor.Invoke(new object { new ImmutableUpdate(updateBuilder._updates) });
}
edited Dec 20 at 19:58
answered Dec 20 at 17:34
Jesse C. Slicer
11.3k2740
11.3k2740
1
Ooops, it stayed there after moving it do the helper ;-]
– t3chb0t
Dec 20 at 17:35
1
You must like it ;-) I see you've added a builder. I'll definitely borrow it.
– t3chb0t
Dec 20 at 20:21
add a comment |
1
Ooops, it stayed there after moving it do the helper ;-]
– t3chb0t
Dec 20 at 17:35
1
You must like it ;-) I see you've added a builder. I'll definitely borrow it.
– t3chb0t
Dec 20 at 20:21
1
1
Ooops, it stayed there after moving it do the helper ;-]
– t3chb0t
Dec 20 at 17:35
Ooops, it stayed there after moving it do the helper ;-]
– t3chb0t
Dec 20 at 17:35
1
1
You must like it ;-) I see you've added a builder. I'll definitely borrow it.
– t3chb0t
Dec 20 at 20:21
You must like it ;-) I see you've added a builder. I'll definitely borrow it.
– t3chb0t
Dec 20 at 20:21
add a 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.
Some of your past answers have not been well-received, and you're in danger of being blocked from answering.
Please pay close attention to the following guidance:
- 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.
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%2f210056%2fimmutable-type-updater-using-a-special-constructor%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
1
I was literally just contemplating writing one of these... I was wondering how best to specify properties, and had completely forgotten about Expressions, so thanks ;)
– VisualMelon
Dec 20 at 15:34
1
At first blush, the chained
With
s that return a new object each time seem a little wasteful if the next one is discarding the previous. Perhaps it could return an "interim" container object that can accumulate theWith
expressions and a finalApply()
that executes them once and produces a singleUser
(in this case) object?– Jesse C. Slicer
Dec 20 at 17:16