diff --git a/csharp-intermediate-topics/DeepCopyInCSharp/DeepCopyInCSharp/Address.cs b/csharp-intermediate-topics/DeepCopyInCSharp/DeepCopyInCSharp/Address.cs index b624685963..91bb86a42f 100644 --- a/csharp-intermediate-topics/DeepCopyInCSharp/DeepCopyInCSharp/Address.cs +++ b/csharp-intermediate-topics/DeepCopyInCSharp/DeepCopyInCSharp/Address.cs @@ -1,4 +1,6 @@ -namespace DeepCopyInCSharp +using System.Diagnostics.CodeAnalysis; + +namespace DeepCopyInCSharp { [Serializable] public class Address : ICloneable @@ -7,6 +9,16 @@ public class Address : ICloneable public required string City { get; set; } public required string State { get; set; } + public Address() { } + + [SetsRequiredMembers] + public Address(Address other) + { + Street = other.Street; + City = other.City; + State = other.State; + } + public object Clone() { return new Address diff --git a/csharp-intermediate-topics/DeepCopyInCSharp/DeepCopyInCSharp/DeepCopierBenchmark.cs b/csharp-intermediate-topics/DeepCopyInCSharp/DeepCopyInCSharp/DeepCopierBenchmark.cs index 988e87561f..6379d8bf93 100644 --- a/csharp-intermediate-topics/DeepCopyInCSharp/DeepCopyInCSharp/DeepCopierBenchmark.cs +++ b/csharp-intermediate-topics/DeepCopyInCSharp/DeepCopyInCSharp/DeepCopierBenchmark.cs @@ -4,7 +4,7 @@ namespace DeepCopyInCSharp { public class DeepCopierBenchmark { - private Person? _person; + private Person _person = null!; [GlobalSetup] public void Setup() @@ -22,6 +22,12 @@ public void Setup() }; } + [Benchmark] + public Person CopyConstructorBenchmark() + { + return new Person(_person); + } + [Benchmark] public Person ICloneableBenchmark() { @@ -58,12 +64,6 @@ public Person ExpressionTreesBenchmark() return DeepCopyMaker.DeepCopyExpressionTrees(_person); } - [Benchmark] - public Person AutoMapperBenchmark() - { - return new DeepCopyMaker().DeepCopyAutoMapper(_person); - } - [Benchmark] public Person FastDeepClonerBenchmark() { diff --git a/csharp-intermediate-topics/DeepCopyInCSharp/DeepCopyInCSharp/DeepCopyInCSharp.csproj b/csharp-intermediate-topics/DeepCopyInCSharp/DeepCopyInCSharp/DeepCopyInCSharp.csproj index 0a3c72bc58..d072e4d083 100644 --- a/csharp-intermediate-topics/DeepCopyInCSharp/DeepCopyInCSharp/DeepCopyInCSharp.csproj +++ b/csharp-intermediate-topics/DeepCopyInCSharp/DeepCopyInCSharp/DeepCopyInCSharp.csproj @@ -2,17 +2,16 @@ Exe - net7.0 + net10.0 enable enable - - + - + diff --git a/csharp-intermediate-topics/DeepCopyInCSharp/DeepCopyInCSharp/DeepCopyMaker.cs b/csharp-intermediate-topics/DeepCopyInCSharp/DeepCopyInCSharp/DeepCopyMaker.cs index b6e8326b9a..657f972e91 100644 --- a/csharp-intermediate-topics/DeepCopyInCSharp/DeepCopyInCSharp/DeepCopyMaker.cs +++ b/csharp-intermediate-topics/DeepCopyInCSharp/DeepCopyInCSharp/DeepCopyMaker.cs @@ -1,5 +1,4 @@ -using AutoMapper; -using DeepCopy; +using DeepCopy; using FastDeepCloner; using System.Linq.Expressions; using System.Runtime.Serialization; @@ -10,62 +9,56 @@ namespace DeepCopyInCSharp { public class DeepCopyMaker { - private readonly IMapper _mapper; - - public DeepCopyMaker() - { - var config = new MapperConfiguration(cfg => - { - cfg.CreateMap(); - cfg.CreateMap() - .ForMember(dest => dest.Address, opt => opt.MapFrom(src => _mapper.Map
(src.Address))); - }); - - _mapper = config.CreateMapper(); - } - public static T DeepCopyXML(T input) { + ArgumentNullException.ThrowIfNull(input); + using var stream = new MemoryStream(); var serializer = new XmlSerializer(typeof(T)); serializer.Serialize(stream, input); stream.Position = 0; - return (T)serializer.Deserialize(stream); + return (T)serializer.Deserialize(stream)!; } public static T DeepCopyJSON(T input) { + ArgumentNullException.ThrowIfNull(input); + var jsonString = JsonSerializer.Serialize(input); - return JsonSerializer.Deserialize(jsonString); + return JsonSerializer.Deserialize(jsonString)!; } public static T DeepCopyDataContract(T input) { + ArgumentNullException.ThrowIfNull(input); + using var stream = new MemoryStream(); var serializer = new DataContractSerializer(typeof(T)); serializer.WriteObject(stream, input); stream.Position = 0; - return (T)serializer.ReadObject(stream); + return (T)serializer.ReadObject(stream)!; } public static T DeepCopyReflection(T input) { + ArgumentNullException.ThrowIfNull(input); + var type = input.GetType(); var properties = type.GetProperties(); - T clonedObj = (T)Activator.CreateInstance(type); + T clonedObj = (T)Activator.CreateInstance(type)!; foreach (var property in properties) { if (property.CanWrite) { - object value = property.GetValue(input); - if (value != null && value.GetType().IsClass && !value.GetType().FullName.StartsWith("System.")) + object? value = property.GetValue(input); + if (value != null && value.GetType().IsClass && !value.GetType().FullName!.StartsWith("System.")) { property.SetValue(clonedObj, DeepCopyReflection(value)); } @@ -81,7 +74,15 @@ public static T DeepCopyReflection(T input) public static T DeepCopyExpressionTrees(T input) { - return GenerateDeepCopy()(input); + return Cache.Copy(input); + } + + // Compiling an expression tree is expensive, so each type's delegate is built once + // and reused. The generated code copies a nested object by calling + // DeepCopyExpressionTrees, so nested types go through this cache as well. + private static class Cache + { + public static readonly Func Copy = GenerateDeepCopy(); } private static Func GenerateDeepCopy() @@ -96,7 +97,7 @@ private static Func GenerateDeepCopy() if (propertyInfo.PropertyType.IsClass && propertyInfo.PropertyType != typeof(string)) { var copyMethod = typeof(DeepCopyMaker) - .GetMethod(nameof(DeepCopyMaker.DeepCopyExpressionTrees)) + .GetMethod(nameof(DeepCopyMaker.DeepCopyExpressionTrees))! .MakeGenericMethod(propertyInfo.PropertyType); var propertyCopyExpression = Expression.Call(copyMethod, propertyExpression); @@ -114,11 +115,6 @@ private static Func GenerateDeepCopy() return Expression.Lambda>(memberInitExpression, inputParameter).Compile(); } - public Person DeepCopyAutoMapper(Person input) - { - return _mapper.Map(input); - } - public static T DeepCopyFastDeepCloner(T input) { return (T)DeepCloner.Clone(input); @@ -131,9 +127,11 @@ public static T DeepCopyLibraryDeepCopy(T input) public static T DeepCopyJsonDotNet(T input) { + ArgumentNullException.ThrowIfNull(input); + var serialized = Newtonsoft.Json.JsonConvert.SerializeObject(input); - return Newtonsoft.Json.JsonConvert.DeserializeObject(serialized); + return Newtonsoft.Json.JsonConvert.DeserializeObject(serialized)!; } } } diff --git a/csharp-intermediate-topics/DeepCopyInCSharp/DeepCopyInCSharp/Person.cs b/csharp-intermediate-topics/DeepCopyInCSharp/DeepCopyInCSharp/Person.cs index 36022b0ca1..6a5caae585 100644 --- a/csharp-intermediate-topics/DeepCopyInCSharp/DeepCopyInCSharp/Person.cs +++ b/csharp-intermediate-topics/DeepCopyInCSharp/DeepCopyInCSharp/Person.cs @@ -1,4 +1,5 @@ -using System.Runtime.Serialization; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.Serialization; namespace DeepCopyInCSharp { @@ -15,6 +16,16 @@ public class Person : ICloneable [DataMember] public required Address Address { get; set; } + public Person() { } + + [SetsRequiredMembers] + public Person(Person other) + { + Name = other.Name; + Age = other.Age; + Address = new Address(other.Address); + } + public Person ShallowCopy() => (Person)this.MemberwiseClone(); public object Clone() diff --git a/csharp-intermediate-topics/DeepCopyInCSharp/DeepCopyInCSharp/Program.cs b/csharp-intermediate-topics/DeepCopyInCSharp/DeepCopyInCSharp/Program.cs index c609059c85..0d626321eb 100644 --- a/csharp-intermediate-topics/DeepCopyInCSharp/DeepCopyInCSharp/Program.cs +++ b/csharp-intermediate-topics/DeepCopyInCSharp/DeepCopyInCSharp/Program.cs @@ -8,6 +8,9 @@ public class Program static void Main(string[] args) { //Benchmark - start + // FastDeepCloner 1.3.6 ships an assembly built without optimizations, and BenchmarkDotNet + // refuses to run while any referenced assembly is non-optimized. This switch turns that + // check off for every assembly, so always run this project with -c Release. var config = ManualConfig.Create(DefaultConfig.Instance) .WithOptions(ConfigOptions.DisableOptimizationsValidator); @@ -15,59 +18,67 @@ static void Main(string[] args) Console.WriteLine(summary); //Benchmark - end - var originalPerson = new Person - { - Name = "Steve Doe", - Age = 21, - Address = new Address - { - Street = "123 Main St.", - City = "Anytown", - State = "AB" - } - }; - //Shallow Copy - var copiedPerson = originalPerson.ShallowCopy(); + CopyModifyAndPrint("Shallow copy (MemberwiseClone)", original => original.ShallowCopy()); //Deep Copy - ICloneable - copiedPerson = (Person)originalPerson.Clone(); + CopyModifyAndPrint("ICloneable", original => (Person)original.Clone()); + + //Deep Copy - Copy Constructor + CopyModifyAndPrint("Copy constructor", original => new Person(original)); //Deep Copy - XML Serializer - copiedPerson = DeepCopyMaker.DeepCopyXML(originalPerson); + CopyModifyAndPrint("XML serialization", DeepCopyMaker.DeepCopyXML); - //Deep Copy - JSON Serialzer - copiedPerson = DeepCopyMaker.DeepCopyJSON(originalPerson); + //Deep Copy - JSON Serializer + CopyModifyAndPrint("JSON serialization", DeepCopyMaker.DeepCopyJSON); //Deep Copy - Data Contract Serialization - copiedPerson = DeepCopyMaker.DeepCopyDataContract(originalPerson); + CopyModifyAndPrint("Data contract serialization", DeepCopyMaker.DeepCopyDataContract); //Deep Copy - Reflection - copiedPerson = DeepCopyMaker.DeepCopyReflection(originalPerson); + CopyModifyAndPrint("Reflection", DeepCopyMaker.DeepCopyReflection); //Deep Copy - Expression Trees - copiedPerson = DeepCopyMaker.DeepCopyExpressionTrees(originalPerson); - - //Deep Copy - AutoMapper - var copier = new DeepCopyMaker(); - copiedPerson = copier.DeepCopyAutoMapper(originalPerson); + CopyModifyAndPrint("Expression trees", DeepCopyMaker.DeepCopyExpressionTrees); //Deep Copy - FastDeepCloner - copiedPerson = DeepCopyMaker.DeepCopyFastDeepCloner(originalPerson); + CopyModifyAndPrint("FastDeepCloner", DeepCopyMaker.DeepCopyFastDeepCloner); //Deep Copy - DeepCopy - copiedPerson = DeepCopyMaker.DeepCopyLibraryDeepCopy(originalPerson); + CopyModifyAndPrint("DeepCopy", DeepCopyMaker.DeepCopyLibraryDeepCopy); //Deep Copy - JSON.Net - copiedPerson = DeepCopyMaker.DeepCopyJsonDotNet(originalPerson); + CopyModifyAndPrint("Json.NET", DeepCopyMaker.DeepCopyJsonDotNet); + } + + // Each technique gets a fresh original, so one technique's result + // can never be hidden behind the next one's. + private static void CopyModifyAndPrint(string technique, Func copy) + { + var originalPerson = new Person + { + Name = "Steve Doe", + Age = 22, + Address = new Address + { + Street = "123 Main St.", + City = "Anytown", + State = "AB" + } + }; + + var copiedPerson = copy(originalPerson); //Modifying the copied object copiedPerson.Name = "Jack Swallow"; copiedPerson.Address.Street = "456 Elmo St."; //Result + Console.WriteLine(technique); Console.WriteLine($"Original Name: {originalPerson.Name}"); Console.WriteLine($"Original Street: {originalPerson.Address.Street}"); + Console.WriteLine(); } } } \ No newline at end of file diff --git a/csharp-intermediate-topics/DeepCopyInCSharp/Tests/Test.cs b/csharp-intermediate-topics/DeepCopyInCSharp/Tests/Test.cs index d6b3f8287a..99be3d81f3 100644 --- a/csharp-intermediate-topics/DeepCopyInCSharp/Tests/Test.cs +++ b/csharp-intermediate-topics/DeepCopyInCSharp/Tests/Test.cs @@ -29,21 +29,22 @@ public void WhenUsingICloneable_ThenAllPropertiesCopied() } [Fact] - public void WhenUsingXMLSerialization_ThenAllPropertiesCopied() + public void WhenUsingCopyConstructor_ThenAllPropertiesCopiedAndAddressNotShared() { - var copy = DeepCopyMaker.DeepCopyXML(_person); + var copy = new Person(_person); Assert.Equal(copy.Name, _person.Name); Assert.Equal(copy.Age, _person.Age); Assert.Equal(copy.Address.Street, _person.Address.Street); Assert.Equal(copy.Address.City, _person.Address.City); Assert.Equal(copy.Address.State, _person.Address.State); + Assert.NotSame(_person.Address, copy.Address); } [Fact] - public void WhenUsingJsonSerialization_ThenAllPropertiesCopied() + public void WhenUsingXMLSerialization_ThenAllPropertiesCopied() { - var copy = DeepCopyMaker.DeepCopyJSON(_person); + var copy = DeepCopyMaker.DeepCopyXML(_person); Assert.Equal(copy.Name, _person.Name); Assert.Equal(copy.Age, _person.Age); @@ -53,9 +54,9 @@ public void WhenUsingJsonSerialization_ThenAllPropertiesCopied() } [Fact] - public void WhenUsingDataContractSerialization_ThenAllPropertiesCopied() + public void WhenUsingJsonSerialization_ThenAllPropertiesCopied() { - var copy = DeepCopyMaker.DeepCopyDataContract(_person); + var copy = DeepCopyMaker.DeepCopyJSON(_person); Assert.Equal(copy.Name, _person.Name); Assert.Equal(copy.Age, _person.Age); @@ -65,9 +66,9 @@ public void WhenUsingDataContractSerialization_ThenAllPropertiesCopied() } [Fact] - public void WhenUsingReflection_ThenAllPropertiesCopied() + public void WhenUsingDataContractSerialization_ThenAllPropertiesCopied() { - var copy = DeepCopyMaker.DeepCopyReflection(_person); + var copy = DeepCopyMaker.DeepCopyDataContract(_person); Assert.Equal(copy.Name, _person.Name); Assert.Equal(copy.Age, _person.Age); @@ -77,9 +78,9 @@ public void WhenUsingReflection_ThenAllPropertiesCopied() } [Fact] - public void WhenUsingExpressionTrees_ThenAllPropertiesCopied() + public void WhenUsingReflection_ThenAllPropertiesCopied() { - var copy = DeepCopyMaker.DeepCopyExpressionTrees(_person); + var copy = DeepCopyMaker.DeepCopyReflection(_person); Assert.Equal(copy.Name, _person.Name); Assert.Equal(copy.Age, _person.Age); @@ -89,10 +90,9 @@ public void WhenUsingExpressionTrees_ThenAllPropertiesCopied() } [Fact] - public void WhenUsingAutoMApper_ThenAllPropertiesCopied() + public void WhenUsingExpressionTrees_ThenAllPropertiesCopied() { - var copier = new DeepCopyMaker(); - var copy = copier.DeepCopyAutoMapper(_person); + var copy = DeepCopyMaker.DeepCopyExpressionTrees(_person); Assert.Equal(copy.Name, _person.Name); Assert.Equal(copy.Age, _person.Age); diff --git a/csharp-intermediate-topics/DeepCopyInCSharp/Tests/Tests.csproj b/csharp-intermediate-topics/DeepCopyInCSharp/Tests/Tests.csproj index 0eb4dbf7fb..3c46d1c456 100644 --- a/csharp-intermediate-topics/DeepCopyInCSharp/Tests/Tests.csproj +++ b/csharp-intermediate-topics/DeepCopyInCSharp/Tests/Tests.csproj @@ -1,7 +1,7 @@ - net7.0 + net10.0 enable enable @@ -9,13 +9,13 @@ - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all