I know you said you didn't want to use reflection, however using reflection in C# it'd be as simple as (very non-optimized code):
public static void UpdateActualFromDto(object actual, object dto) {
var dtoProps = dto.GetType().GetProperties();
var actualProps = actual.GetType().GetProperties();
var sameProperties = dtoProps.Where(x => actualProps.Select(a => a.Name).Contains(x.Name)).ToDictionary(x => x, x => actualProps.First(p => p.Name == x.Name));
foreach(var (dtoProp, actualProp) in sameProperties) {
var val = dtoProp.GetValue(dto);
if(val != null) actualProp.SetValue(actual, val);
}
}
And just for fun, I benchmarked it (again, this is hardly optimized, you could even have a cache of PropertyInfo
and make it way faster):
BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042
Intel Core i7-9700K CPU 3.60GHz (Coffee Lake), 1 CPU, 8 logical and 8 physical cores
.NET Core SDK=5.0.102
[Host] : .NET Core 3.1.11 (CoreCLR 4.700.20.56602, CoreFX 4.700.20.56604), X64 RyuJIT [AttachedDebugger]
DefaultJob : .NET Core 3.1.11 (CoreCLR 4.700.20.56602, CoreFX 4.700.20.56604), X64 RyuJIT
Method |
Mean |
Error |
StdDev |
CopyNonNull |
899.7 ns |
3.24 ns |
2.71 ns |
The benchmarked code is:
public class Dto {
public string PropertyA { get; set; }
public int? PropertyB { get; set; }
}
public class Actual {
public string PropertyA { get; set; }
public int PropertyB { get; set; }
}
public class CopyDtoBenchmark
{
[Benchmark]
public void CopyNonNull()
{
UpdateActualFromDto(new Actual { PropertyA = "bar", PropertyB = 5 }, new Dto { PropertyA = "foo" });
}
// UpdateActualFromDto method above
}
Again, no caching, and the benchmark is creating and allocating the objects (botht he actual and the dto).
Just for fun, I added some caching for matching dto/entity types, and times were reduced (for this particular benchmark that uses the same types all the time) to:
Method |
Mean |
Error |
StdDev |
CopyNonNull |
322.8 ns |
1.28 ns |
1.20 ns |
Updated code:
private Dictionary<(Type, Type), Dictionary<PropertyInfo, PropertyInfo>> _typeMatchCache =
new();
public void UpdateActualFromDto(object actual, object dto)
{
var dtoType = dto.GetType();
var actualType = actual.GetType();
if (!_typeMatchCache.TryGetValue((dtoType, actualType), out var sameProperties))
{
var dtoProps = dtoType.GetProperties();
var actualProps = actualType.GetProperties();
sameProperties = dtoProps.Where(x => actualProps.Select(a => a.Name).Contains(x.Name)).ToDictionary(x => x, x => actualProps.First(p => p.Name == x.Name));
_typeMatchCache.Add((dtoType, actualType), sameProperties);
}
foreach(var (dtoProp, actualProp) in sameProperties) {
var val = dtoProp.GetValue(dto);
if(val != null) actualProp.SetValue(actual, val);
}
}
Again, this is a just for fun experiment, and if you were to use code like this you'd probably need better checks (check if types are the same, etc.), but I wouldn't discard reflection saying it's "cost heavy" without actually trying if the performance is good enough and whether the CPU cost of using it vs the cost of programming (and more importantly, maintaining, every time any of the involved classes changes) a non-generic solution is worth it.
By the way, I wouldn't recommend using this and I'd probably go for something like Automapper for it (but that WILL use reflection)