After
foo2.Bar = "test2";
foo2.Bar
points to a different string. The assignment changes which object the reference points to (as opposed to making changes to the object that the reference points to):
var foo1 = new Foo();
foo1.Bar = "test";
//
// "test"
// foo1.Bar ---------------------┘
var foo2 = foo1;
//
// "test"
// foo1.Bar ---------------------┘ |
// |
// foo2.Bar ------------------------┘
foo2.Bar = "test2";
//
// "test"
// foo1.Bar ---------------------┘
//
// "test2"
// foo2.Bar ---------------------┘
This is not specific to strings. Here's an example with a list (inspired by Value types (C# reference)):
A a1 = new A() { L = new List<string> {"1", "11" } };
A a2 = a1; // Shallow copy
Console.WriteLine(a1); // [1,11]
Console.WriteLine(a2); // [1,11]
a2.L.Add("X");
Console.WriteLine(a1); // [1,11,X]
Console.WriteLine(a2); // [1,11,X]
// this does not make changes to the object that a2.L points to,
// it changes which object a2.L points to.
a2.L = new List<string> {"2", "22" };
Console.WriteLine(a1); // [1,11,X]
Console.WriteLine(a2); // [2,22]
public struct A
{
public List<string> L {get; set; }
public override string ToString() => $"[{string.Join(",", L)}]";
}
Please note that your question is NOT about immutability of strings. This is because nowhere in your code the strings are modified.
From Strings and string literals:
Because a string "modification" is actually a new string creation, you must use caution when you create references to strings. If you create a reference to a string, and then "modify" the original string, the reference will continue to point to the original object instead of the new object that was created when the string was modified. The following code illustrates this behavior:
string str1 = "Hello ";
string str2 = str1;
str1 += "World";
System.Console.WriteLine(str2);
//Output: Hello
^ This is about changing strings and your example does not change strings.
Reference for value type assignment:
From Value types (C# reference):
By default, on assignment, passing an argument to a method, and returning a method result, variable values are copied.
and
If a value type contains a data member of a reference type, only the reference to the instance of the reference type is copied when a value-type instance is copied. Both the copy and original value-type instance have access to the same reference-type instance.