-
Notifications
You must be signed in to change notification settings - Fork 15
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
#44 - Added the with extensions code back in the new v4/C# 8 branch
- Loading branch information
Showing
6 changed files
with
860 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Reflection; | ||
|
||
namespace SuccincT.Functional | ||
{ | ||
internal class CachedConstructorInfo | ||
{ | ||
public ConstructorInfo Constructor { get; } | ||
public List<ParameterInfo> Parameters { get; } | ||
|
||
public CachedConstructorInfo(ConstructorInfo constructorInfo) | ||
=> (Constructor, Parameters) = (constructorInfo, constructorInfo.GetParameters().ToList()); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Reflection; | ||
|
||
namespace SuccincT.Functional | ||
{ | ||
internal class CachedTypeInfo | ||
{ | ||
public List<CachedConstructorInfo> CachedPublicConstructors { get; } | ||
public List<PropertyInfo> Properties { get; } | ||
public List<PropertyInfo> ReadOnlyProperties { get; } | ||
public List<PropertyInfo> WriteOnlyProperties { get; } | ||
|
||
public CachedTypeInfo(Type type) | ||
{ | ||
var typeInfo = type.GetTypeInfo(); | ||
|
||
var cachedConstructors = | ||
typeInfo.DeclaredConstructors.Select(c => new CachedConstructorInfo(c)).ToList(); | ||
|
||
CachedPublicConstructors = cachedConstructors | ||
.Where(cc => cc.Constructor.IsPublic && !cc.Constructor.IsStatic).ToList(); | ||
|
||
Properties = type.GetRuntimeProperties().ToList(); | ||
ReadOnlyProperties = Properties.Where(p => p.CanRead && !p.CanWrite).ToList(); | ||
WriteOnlyProperties = Properties.Where(p => !p.CanRead && p.CanWrite).ToList(); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
using System; | ||
|
||
namespace SuccincT.Functional | ||
{ | ||
public sealed class CopyException : Exception | ||
{ | ||
public CopyException(string message) : base(message) { } | ||
|
||
public CopyException(string message, Exception innerException) : base(message, innerException) { } | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,331 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Reflection; | ||
using SuccincT.Options; | ||
|
||
namespace SuccincT.Functional | ||
{ | ||
public static class WithExtensions | ||
{ | ||
private static readonly Dictionary<string, CachedTypeInfo> CachedTypeInfoDetails = | ||
new Dictionary<string, CachedTypeInfo>(); | ||
|
||
public static Option<T> TryCopy<T>(this T @object) where T : notnull | ||
{ | ||
var cachedTypeInfo = GetCachedTypeInfo(typeof(T)); | ||
|
||
var (hasValue, value) = cachedTypeInfo.CachedPublicConstructors | ||
.OrderByDescending(cc => cc.Parameters.Count) | ||
.TryFirst(); | ||
|
||
if (!hasValue) return Option<T>.None(); | ||
|
||
var sourceReadProperties = cachedTypeInfo.Properties.Except(cachedTypeInfo.WriteOnlyProperties).ToList(); | ||
var constructorParameters = value.Parameters; | ||
|
||
var constructorParameterValues = | ||
GetConstructorParameterValuesForCopy(@object, sourceReadProperties, constructorParameters); | ||
|
||
if (constructorParameterValues.Length != constructorParameters.Count) return Option<T>.None(); | ||
|
||
var newObject = Activator.CreateInstance(typeof(T), constructorParameterValues); | ||
var destWriteProperties = cachedTypeInfo.Properties.Except(cachedTypeInfo.ReadOnlyProperties); | ||
|
||
var propertiesToOverwrite = sourceReadProperties | ||
.Select(p => destWriteProperties.TryFirst(x => p.Name == x.Name)) | ||
.Where(x => x.HasValue) | ||
.Select(x => x.Value); | ||
|
||
foreach (var propertyToOverwrite in propertiesToOverwrite) | ||
{ | ||
CopyPropertyValue(@object, propertyToOverwrite, newObject); | ||
} | ||
|
||
return new Option<T>((T)newObject); | ||
} | ||
|
||
public static T Copy<T>(this T @object) where T : notnull | ||
{ | ||
var cachedTypeInfo = GetCachedTypeInfo(typeof(T)); | ||
|
||
var (hasValue, value) = cachedTypeInfo.CachedPublicConstructors | ||
.OrderByDescending(cc => cc.Parameters.Count) | ||
.TryFirst(); | ||
|
||
if (!hasValue) | ||
{ | ||
throw new CopyException( | ||
$"Type {typeof(T).Name} does not supply a public constructor for use with Copy."); | ||
} | ||
|
||
var sourceReadProperties = cachedTypeInfo.Properties.Except(cachedTypeInfo.WriteOnlyProperties).ToList(); | ||
var constructorParameters = value.Parameters; | ||
|
||
var constructorParameterValues = | ||
GetConstructorParameterValuesForCopy(@object, sourceReadProperties, constructorParameters); | ||
|
||
if (constructorParameterValues.Length != constructorParameters.Count) | ||
{ | ||
throw new CopyException( | ||
$"Type {typeof(T).Name} does not supply a suitable constructor for use with Copy, which allows all " + | ||
"non-writable properties to be set via that constructor."); | ||
} | ||
|
||
var newObject = Activator.CreateInstance(typeof(T), constructorParameterValues); | ||
var destWriteProperties = cachedTypeInfo.Properties.Except(cachedTypeInfo.ReadOnlyProperties); | ||
|
||
var propertiesToOverwrite = sourceReadProperties | ||
.Select(p => destWriteProperties.TryFirst(x => p.Name == x.Name)) | ||
.Where(x => x.HasValue) | ||
.Select(x => x.Value); | ||
|
||
foreach (var propertyToOverwrite in propertiesToOverwrite) | ||
{ | ||
CopyPropertyValue(@object, propertyToOverwrite, newObject); | ||
} | ||
|
||
return (T)newObject; | ||
} | ||
|
||
public static Option<T> TryWith<T, TProps>(this T itemToCopy, TProps propertiesToUpdate) | ||
where T : notnull where TProps : class | ||
{ | ||
if (propertiesToUpdate == null) return Option<T>.None(); | ||
|
||
var cachedTypeInfo = GetCachedTypeInfo(typeof(T)); | ||
var sourceReadProperties = cachedTypeInfo.Properties.Except(cachedTypeInfo.WriteOnlyProperties).ToList(); | ||
var updateProperties = typeof(TProps).GetRuntimeProperties().Where(x => x.CanRead).ToList(); | ||
var (hasValue, value) = ConstructorToUseForWith(cachedTypeInfo, updateProperties, sourceReadProperties); | ||
|
||
if (!hasValue) return Option<T>.None(); | ||
|
||
var constructorParameters = value.Parameters; | ||
var constructorParameterValues = MapUpdateValuesToConstructorParameters(itemToCopy, | ||
propertiesToUpdate, | ||
constructorParameters, | ||
updateProperties, | ||
sourceReadProperties); | ||
|
||
var destWriteProperties = cachedTypeInfo.Properties.Except(cachedTypeInfo.ReadOnlyProperties); | ||
var propsToSetFromUpdateData = GetPropertiesToSetFromUpdateData(updateProperties, | ||
constructorParameters, | ||
sourceReadProperties); | ||
|
||
var propsToSetFromSourceObject = GetPropertiesToSetFromSourceObject(sourceReadProperties, | ||
constructorParameters, | ||
propsToSetFromUpdateData, | ||
destWriteProperties); | ||
|
||
try | ||
{ | ||
return Option<T>.Some(CreateNewObjectApplyingUpdates(itemToCopy, | ||
propertiesToUpdate, | ||
constructorParameterValues, | ||
propsToSetFromSourceObject, | ||
propsToSetFromUpdateData)); | ||
} | ||
catch (Exception) | ||
{ | ||
return Option<T>.None(); | ||
} | ||
} | ||
|
||
public static T With<T, TProps>(this T itemToCopy, TProps propertiesToUpdate) | ||
where T : notnull where TProps : class | ||
{ | ||
if (propertiesToUpdate == null) throw new ArgumentNullException(nameof(propertiesToUpdate)); | ||
|
||
var cachedTypeInfo = GetCachedTypeInfo(typeof(T)); | ||
var sourceReadProperties = cachedTypeInfo.Properties.Except(cachedTypeInfo.WriteOnlyProperties).ToList(); | ||
var updateProperties = typeof(TProps).GetRuntimeProperties().Where(x => x.CanRead).ToList(); | ||
var (hasValue, value) = ConstructorToUseForWith(cachedTypeInfo, updateProperties, sourceReadProperties); | ||
|
||
if (!hasValue) | ||
{ | ||
throw new CopyException( | ||
$"Type {typeof(T).Name} does not supply a suitable constructor for use with With, which allows all " + | ||
"non-writable properties to be set via that constructor."); | ||
} | ||
|
||
var constructorParameters = value.Parameters; | ||
var constructorParameterValues = MapUpdateValuesToConstructorParameters(itemToCopy, | ||
propertiesToUpdate, | ||
constructorParameters, | ||
updateProperties, | ||
sourceReadProperties); | ||
|
||
var destWriteProperties = cachedTypeInfo.Properties.Except(cachedTypeInfo.ReadOnlyProperties); | ||
var propsToSetFromUpdateData = GetPropertiesToSetFromUpdateData(updateProperties, | ||
constructorParameters, | ||
sourceReadProperties); | ||
|
||
var propsToSetFromSourceObject = GetPropertiesToSetFromSourceObject(sourceReadProperties, | ||
constructorParameters, | ||
propsToSetFromUpdateData, | ||
destWriteProperties); | ||
|
||
try | ||
{ | ||
return CreateNewObjectApplyingUpdates(itemToCopy, | ||
propertiesToUpdate, | ||
constructorParameterValues, | ||
propsToSetFromSourceObject, | ||
propsToSetFromUpdateData); | ||
} | ||
catch (Exception ex) | ||
{ | ||
throw new CopyException($"A problem occurred creating a new instance of {typeof(T).Name} using With." + | ||
"See the inner exception for details of the problem.", | ||
ex); | ||
} | ||
} | ||
|
||
private static object[] GetConstructorParameterValuesForCopy<T>( | ||
T @object, | ||
IEnumerable<PropertyInfo> sourceReadProperties, | ||
IEnumerable<ParameterInfo> | ||
constructorParameters) where T : notnull | ||
{ | ||
return constructorParameters.Select(p => sourceReadProperties.TryFirst(x => AreLinked(x, p))) | ||
.Where(x => x.HasValue).Select(x => x.Value) | ||
.Select(sourceReadProperty => sourceReadProperty.GetValue(@object, null)) | ||
.ToArray(); | ||
} | ||
|
||
private static T CreateNewObjectApplyingUpdates<T, TProps>( | ||
T itemToCopy, | ||
TProps propertiesToUpdate, | ||
object[] constructorParameterValues, | ||
IEnumerable<PropertyInfo> propsToSetFromSourceObject, | ||
IEnumerable<(PropertyInfo Value, PropertyInfo PropToUpdate)> propsToSetFromUpdateData) | ||
where T : notnull where TProps : class | ||
{ | ||
var newObject = Activator.CreateInstance(typeof(T), constructorParameterValues); | ||
|
||
foreach (var propertyToOverwrite in propsToSetFromSourceObject) | ||
{ | ||
CopyPropertyValue(itemToCopy, propertyToOverwrite, newObject); | ||
} | ||
|
||
foreach (var (sourceProp, propToUpdate) in propsToSetFromUpdateData) | ||
{ | ||
CopyPropertyValue(propertiesToUpdate, propToUpdate, newObject, sourceProp); | ||
} | ||
|
||
return (T)newObject; | ||
} | ||
|
||
private static List<PropertyInfo> GetPropertiesToSetFromSourceObject( | ||
IEnumerable<PropertyInfo> sourceReadProperties, | ||
List<ParameterInfo> constructorParameters, | ||
List<(PropertyInfo Value, PropertyInfo PropToUpdate)> propsToSetFromUpdateData, | ||
IEnumerable<PropertyInfo> destWriteProperties) | ||
{ | ||
return sourceReadProperties | ||
.Where(p => !constructorParameters.Any(cp => AreLinked(cp, p))) | ||
.Where(p => !propsToSetFromUpdateData.Any(tp => AreLinked(p, tp.PropToUpdate))) | ||
.Select(sourceProperty => | ||
destWriteProperties.TryFirst( | ||
destProperty => AreLinked(sourceProperty, destProperty))) | ||
.Where(x => x.HasValue) | ||
.Select(x => x.Value).ToList(); | ||
} | ||
|
||
private static List<(PropertyInfo Value, PropertyInfo PropToUpdate)> GetPropertiesToSetFromUpdateData( | ||
IEnumerable<PropertyInfo> updateProperties, | ||
List<ParameterInfo> constructorParameters, | ||
List<PropertyInfo> sourceReadProperties) | ||
{ | ||
return updateProperties | ||
.Where(p => !constructorParameters.Any(cp => AreLinked(cp, p))) | ||
.Select(propToUpdate => | ||
( | ||
SourceProp: sourceReadProperties.TryFirst( | ||
sp => AreLinked(sp, propToUpdate)), | ||
PropToUpdate: propToUpdate | ||
) | ||
) | ||
.Where(x => x.SourceProp.HasValue && x.SourceProp.Value.CanWrite) | ||
.Select(x => (x.SourceProp.Value, x.PropToUpdate)).ToList(); | ||
} | ||
|
||
private static object[] MapUpdateValuesToConstructorParameters<T, TProps>( | ||
T @object, | ||
TProps propertiesToUpdate, | ||
List<ParameterInfo> constructorParameters, | ||
List<PropertyInfo> updateProperties, | ||
List<PropertyInfo> sourceReadProperties) where T : notnull where TProps : class | ||
{ | ||
return constructorParameters | ||
.Select(p => { | ||
return updateProperties | ||
.TryFirst(ptu => AreLinked(ptu, p)) | ||
.Match<Option<object>>() | ||
.Some().Do(ptu => Option<object>.Some(ptu.GetValue(propertiesToUpdate, null))) | ||
.None().Do(() => { | ||
return sourceReadProperties | ||
.TryFirst(sp => AreLinked(sp, p)) | ||
.Match<Option<object>>() | ||
.Some().Do(sp => Option<object>.Some(sp.GetValue(@object, null))) | ||
.None().Do(Option<object>.None) | ||
.Result(); | ||
}).Result(); | ||
}) | ||
.Where(x => x.HasValue) | ||
.Select(x => x.Value) | ||
.ToArray(); | ||
} | ||
|
||
private static Option<CachedConstructorInfo> ConstructorToUseForWith(CachedTypeInfo cachedTypeInfo, | ||
IEnumerable<PropertyInfo> updateProperties, | ||
IEnumerable<PropertyInfo> readProperties) | ||
{ | ||
return (from constructor in cachedTypeInfo.CachedPublicConstructors | ||
let paramsNotCoveredByUpdates = | ||
constructor.Parameters.Where(p => !updateProperties.Any(ptu => AreLinked(ptu, p))) | ||
let remainingParamsNotCoveredByProperties = | ||
paramsNotCoveredByUpdates.Where(p => !readProperties.Any(rp => AreLinked(rp, p))).ToList() | ||
where !remainingParamsNotCoveredByProperties.Any() | ||
orderby constructor.Parameters.Count descending | ||
select constructor).TryFirst(); | ||
} | ||
|
||
private static CachedTypeInfo GetCachedTypeInfo(Type type) | ||
=> CachedTypeInfoDetails.GetOrAddValue(type.FullName, () => new CachedTypeInfo(type)); | ||
|
||
private static T GetOrAddValue<T>(this Dictionary<string, T> dictionary, string key, Func<T> createValue) | ||
{ | ||
return dictionary.TryGetValue(key) | ||
.Match<T>() | ||
.Some().Do(value => value) | ||
.None().Do(() => | ||
{ | ||
var value = createValue(); | ||
dictionary.Add(key, value); | ||
return value; | ||
}) | ||
.Result(); | ||
} | ||
|
||
private static bool AreLinked(MemberInfo memberInfo, ParameterInfo parameterInfo) | ||
=> string.Equals(memberInfo.Name, parameterInfo.Name, StringComparison.CurrentCultureIgnoreCase); | ||
|
||
private static bool AreLinked(MemberInfo memberInfo, PropertyInfo propertyInfo) => | ||
string.Equals(memberInfo.Name, propertyInfo.Name, StringComparison.CurrentCultureIgnoreCase); | ||
|
||
private static bool AreLinked(ParameterInfo parameterInfo, PropertyInfo propertyInfo) => | ||
string.Equals(parameterInfo.Name, propertyInfo.Name, StringComparison.CurrentCultureIgnoreCase); | ||
|
||
private static void CopyPropertyValue<T>(T from, PropertyInfo property, T to) where T : class | ||
=> property.SetValue(to, property.GetValue(from, null)); | ||
|
||
private static void CopyPropertyValue<T1, T2>(T1 from, | ||
PropertyInfo fromProperty, | ||
T2 to, | ||
PropertyInfo toProperty) where T1 : class where T2 : class | ||
{ | ||
toProperty.SetValue(to, fromProperty.GetValue(from, null)); | ||
} | ||
} | ||
} |
Oops, something went wrong.