Boxing value type to send it to a method and get the result

I don't want to modify the signature or create a wrapper class, I want to just box the value.

Then that will be a problem. Your method passes a boxed int, then unboxes it and adds 3 to the local age2, which causes another boxing operation, and then throws away the value. De-facto, you're assinging age2 to two different objects on the heap, they do not point to the same object. Without modifying the method signature, this won't be possible.

If you look at the generated IL for AddThree, you'll see this clearly:

AddThree:
IL_0000:  nop         
IL_0001:  ldarg.0     
IL_0002:  unbox.any   System.Int32 // unbox age2
IL_0007:  ldc.i4.3    // load 3
IL_0008:  add         // add the two together
IL_0009:  box         System.Int32 // box the result
IL_000E:  starg.s     00 
IL_0010:  ret    

You unbox the value, add 3 and then box the value again, but you never return it.

To visualize this case further, try returning the newly boxed value from the method (just for the sake of the test), and use object.ReferenceEquals to compare them both:

static void Main(string[] args)
{
    int age = 3;
    object myBox = age;
    var otherBox = AddThree(myBox);
    Console.WriteLine(object.ReferenceEquals(otherBox, myBox)); // False
}

private static object AddThree(object age2)
{
    age2 = (int)age2 + 3;
    return age2;
}

Boxed references meant to be immutable. For example, this will not compile:

((Point)p).X += 3; // CS0445: Cannot modify the result of an unboxing conversion.

As the others said, this line causes a pair of boxing and unboxing operation, which ends up in a new reference:

age2 = (int)age2 + 3;

So even though a boxed int is actually a reference the line above modifies the object reference as well, so the caller will still see the same content unless the object itself is passed by reference.

However, there are a few ways for dereferencing and changing a boxed value without changing the reference (none of them recommended, though).

Solution 1:

The simplest way is via reflection. This seems a bit silly because the Int32.m_value field is the int value itself but this allows you to access the int directly.

private static void AddThree(object age2)
{
    FieldInfo intValue = typeof(int).GetTypeInfo().GetDeclaredField("m_value");
    intValue.SetValue(age2, (int)age2 + 3);
}

Solution 2:

This is a much bigger hack and involves the use of the mainly undocumented TypedReference and the __makeref() operator but more or less this is what happens in the background in the first solution:

private static unsafe void AddThree(object age2)
{
    // pinning is required to prevent GC reallocating the object during the pointer operations
    var objectPinned = GCHandle.Alloc(age2, GCHandleType.Pinned);
    try
    {
        // The __makeref() operator returns a TypedReference.
        // It is basically a pair of pointers for the reference value and type.
        TypedReference objRef = __makeref(age2);

        // Dereference it to access the boxed value like this: objRef.Value->object->boxed content
        // For more details see the memory layout of objects: https://blogs.msdn.microsoft.com/seteplia/2017/05/26/managed-object-internals-part-1-layout/
        int* rawContent = (int*)*(IntPtr*)*(IntPtr*)&objRef;

        // rawContent now points to the type handle (just another pointer to the method table).
        // The actual instance fields start after these 4 or 8 bytes depending on the pointer size:
        int* boxedInt = rawContent + (IntPtr.Size == 4 ? 1 : 2);
        *boxedInt += 3;
    }
    finally
    {
        objectPinned.Free();
    }
}