diff --git a/src/NSubstitute/NSubstitute.csproj b/src/NSubstitute/NSubstitute.csproj
index 5f4111434..8b6dd3fd7 100644
--- a/src/NSubstitute/NSubstitute.csproj
+++ b/src/NSubstitute/NSubstitute.csproj
@@ -36,7 +36,7 @@
-
+
diff --git a/src/NSubstitute/Proxies/CastleDynamicProxy/CastleDynamicProxyFactory.cs b/src/NSubstitute/Proxies/CastleDynamicProxy/CastleDynamicProxyFactory.cs
index b7c66a837..7dc3e3856 100644
--- a/src/NSubstitute/Proxies/CastleDynamicProxy/CastleDynamicProxyFactory.cs
+++ b/src/NSubstitute/Proxies/CastleDynamicProxy/CastleDynamicProxyFactory.cs
@@ -46,12 +46,19 @@ public object GenerateProxy(ICallRouter callRouter, Type typeToProxy, Type[] add
}
///
- /// Allows to dynamically create a type in runtime. Returns an instance of ,
- /// so type could be customized and built later.
+ /// Allows to dynamically create a type in runtime.
+ /// Use the callback to define and build the type.
///
- public TypeBuilder DefineDynamicType(string typeName, TypeAttributes flags)
+ /// Callback used to construct the type.
+ /// The result returned by callback.
+ public Type DefineDynamicType(Func typeBuildCallback)
{
- return _proxyGenerator.ProxyBuilder.ModuleScope.DefineType(true, typeName, flags);
+ var moduleBuilder = _proxyGenerator.ProxyBuilder.ModuleScope.ObtainDynamicModuleWithStrongName();
+
+ using (_proxyGenerator.ProxyBuilder.ModuleScope.Lock.ForWriting())
+ {
+ return typeBuildCallback.Invoke(moduleBuilder);
+ }
}
private object CreateProxyUsingCastleProxyGenerator(Type typeToProxy, Type[] additionalInterfaces,
diff --git a/src/NSubstitute/Proxies/DelegateProxy/DelegateProxyFactory.cs b/src/NSubstitute/Proxies/DelegateProxy/DelegateProxyFactory.cs
index b4931bfea..03ebbc9e7 100644
--- a/src/NSubstitute/Proxies/DelegateProxy/DelegateProxyFactory.cs
+++ b/src/NSubstitute/Proxies/DelegateProxy/DelegateProxyFactory.cs
@@ -14,6 +14,7 @@ namespace NSubstitute.Proxies.DelegateProxy
public class DelegateProxyFactory : IProxyFactory
{
private const string MethodNameInsideProxyContainer = "Invoke";
+ private const string IsReadOnlyAttributeFullTypeName = "System.Runtime.CompilerServices.IsReadOnlyAttribute";
private readonly CastleDynamicProxyFactory _castleObjectProxyFactory;
private readonly ConcurrentDictionary _delegateContainerCache = new ConcurrentDictionary();
private long _typeSuffixCounter;
@@ -39,7 +40,7 @@ public object GenerateProxy(ICallRouter callRouter, Type typeToProxy, Type[] add
return DelegateProxy(typeToProxy, callRouter);
}
- private bool HasItems(T[] array)
+ private static bool HasItems(T[] array)
{
return array != null && array.Length > 0;
}
@@ -67,31 +68,95 @@ private Type GenerateDelegateContainerInterface(Type delegateType)
delegateTypeName,
typeSuffixCounter.ToString(CultureInfo.InvariantCulture));
- var typeBuilder = _castleObjectProxyFactory.DefineDynamicType(
- typeName,
- TypeAttributes.Abstract | TypeAttributes.Interface | TypeAttributes.Public);
+ return _castleObjectProxyFactory.DefineDynamicType(moduleBuilder =>
+ {
+ var typeBuilder = moduleBuilder.DefineType(
+ typeName,
+ TypeAttributes.Abstract | TypeAttributes.Interface | TypeAttributes.Public);
+
+ // Notice, we don't copy the custom modifiers here.
+ // That's absolutely fine, as custom modifiers are ignored when delegate is constructed.
+ // See the related discussion here: https://github.com/dotnet/coreclr/issues/18401
+ var methodBuilder = typeBuilder
+ .DefineMethod(
+ MethodNameInsideProxyContainer,
+ MethodAttributes.Abstract | MethodAttributes.Virtual | MethodAttributes.Public,
+ CallingConventions.Standard,
+ delegateSignature.ReturnType,
+ delegateSignature.GetParameters().Select(p => p.ParameterType).ToArray());
+
+ // Copy original method attributes, so "out" parameters are recognized later.
+ for (var i = 0; i < delegateParameters.Length; i++)
+ {
+ var parameter = delegateParameters[i];
+
+ // Increment position by 1 to skip the implicit "this" parameter.
+ var paramBuilder = methodBuilder.DefineParameter(i + 1, parameter.Attributes, parameter.Name);
+
+ // Read-only parameter ('in' keyword) is recognized by presence of the special attribute.
+ // If source parameter contained that attribute, ensure to copy it to the generated method.
+ // That helps Castle to understand that parameter is read-only and cannot be mutated.
+ DefineIsReadOnlyAttributeIfNeeded(parameter, paramBuilder, moduleBuilder);
+ }
+
+ // Preserve the original delegate type in attribute, so it can be retrieved later in code.
+ methodBuilder.SetCustomAttribute(
+ new CustomAttributeBuilder(
+ typeof(ProxiedDelegateTypeAttribute).GetConstructors().Single(),
+ new object[] {delegateType}));
+
+ return typeBuilder.CreateTypeInfo().AsType();
+ });
+ }
+
+ private static void DefineIsReadOnlyAttributeIfNeeded(
+ ParameterInfo sourceParameter, ParameterBuilder paramBuilder, ModuleBuilder dynamicModuleBuilder)
+ {
+ // Read-only parameter can be by-ref only.
+ if (!sourceParameter.ParameterType.IsByRef)
+ {
+ return;
+ }
+
+ // Lookup for the attribute using full type name.
+ // That's required because compiler can embed that type directly to the client's assembly
+ // as type identity doesn't matter - only full type attribute name is checked.
+ var isReadOnlyAttrType = sourceParameter.CustomAttributes
+ .Select(ca => ca.AttributeType)
+ .FirstOrDefault(t => t.FullName.Equals(IsReadOnlyAttributeFullTypeName, StringComparison.Ordinal));
- var methodBuilder = typeBuilder
- .DefineMethod(
- MethodNameInsideProxyContainer,
- MethodAttributes.Abstract | MethodAttributes.Virtual | MethodAttributes.Public,
- delegateSignature.ReturnType,
- delegateParameters.Select(p => p.ParameterType).ToArray());
+ // Parameter doesn't contain the IsReadOnly attribute.
+ if (isReadOnlyAttrType == null)
+ {
+ return;
+ }
- // Copy original method attributes, so "out" parameters are recognized later.
- for (var i = 0; i < delegateParameters.Length; i++)
+ // If the compiler generated attribute is used (e.g. runtime doesn't contain the attribute),
+ // the generated attribute type might be internal, so we cannot referecnce it in the dynamic assembly.
+ // In this case use the attribute type from the dynamic assembly.
+ if (!isReadOnlyAttrType.GetTypeInfo().IsVisible)
{
- // Increment position by 1 to skip the implicit "this" parameter.
- methodBuilder.DefineParameter(i + 1, delegateParameters[i].Attributes, delegateParameters[i].Name);
+ isReadOnlyAttrType = GetIsReadOnlyAttributeInDynamicModule(dynamicModuleBuilder);
}
- // Preserve the original delegate type in attribute, so it can be retrieved later in code.
- methodBuilder.SetCustomAttribute(
- new CustomAttributeBuilder(
- typeof(ProxiedDelegateTypeAttribute).GetConstructors().Single(),
- new object[] {delegateType}));
+ paramBuilder.SetCustomAttribute(
+ new CustomAttributeBuilder(isReadOnlyAttrType.GetConstructor(Type.EmptyTypes), new object[0]));
+ }
+
+ private static Type GetIsReadOnlyAttributeInDynamicModule(ModuleBuilder moduleBuilder)
+ {
+ var existingType = moduleBuilder.Assembly.GetType(IsReadOnlyAttributeFullTypeName, throwOnError: false, ignoreCase: false);
+ if (existingType != null)
+ {
+ return existingType;
+ }
- return typeBuilder.CreateTypeInfo().AsType();
+ return moduleBuilder
+ .DefineType(
+ IsReadOnlyAttributeFullTypeName,
+ TypeAttributes.Class | TypeAttributes.Sealed | TypeAttributes.NotPublic,
+ typeof(Attribute))
+ .CreateTypeInfo().AsType();
}
}
}
\ No newline at end of file
diff --git a/tests/NSubstitute.Acceptance.Specs/FieldReports/Issue378_InValueTypes.cs b/tests/NSubstitute.Acceptance.Specs/FieldReports/Issue378_InValueTypes.cs
index 3c4905e91..33e78aa1a 100644
--- a/tests/NSubstitute.Acceptance.Specs/FieldReports/Issue378_InValueTypes.cs
+++ b/tests/NSubstitute.Acceptance.Specs/FieldReports/Issue378_InValueTypes.cs
@@ -1,5 +1,4 @@
-using System;
-using NUnit.Framework;
+using NUnit.Framework;
namespace NSubstitute.Acceptance.Specs.FieldReports
{
@@ -8,22 +7,91 @@ namespace NSubstitute.Acceptance.Specs.FieldReports
///
public class Issue378_InValueTypes
{
- public readonly struct Struct { }
+ public readonly struct Struct
+ {
+ public Struct(int value)
+ {
+ Value = value;
+ }
+
+ public int Value { get; }
+ }
+
+ public interface IStructByReadOnlyRefConsumer { void Consume(in Struct value); }
+
+ public interface IStructByValueConsumer { void Consume(Struct value); }
- public interface IStructByRefConsumer { void Consume(in Struct message); }
+ public delegate void DelegateStructByReadOnlyRefConsumer(in Struct value);
- public interface IStructByValueConsumer { void Consume(Struct message); }
+ public delegate void DelegateStructByReadOnlyRefConsumerMultipleArgs(in Struct value1, in Struct value2);
[Test]
- public void IStructByRefConsumer_Test()
+ public void IStructByReadOnlyRefConsumer_Test()
{
- _ = Substitute.For();
+ var value = new Struct(42);
+
+ var subs = Substitute.For();
+ subs.Consume(in value);
}
[Test]
public void IStructByValueConsumer_Test()
{
- _ = Substitute.For();
+ var value = new Struct(42);
+
+ var subs = Substitute.For();
+ subs.Consume(value);
+ }
+
+ [Test]
+ public void DelegateByReadOnlyRefConsumer_Test()
+ {
+ var value = new Struct(42);
+
+ var subs = Substitute.For();
+ subs.Invoke(in value);
+ }
+
+ [Test]
+ public void InterfaceReadOnlyRefCannotBeModified()
+ {
+ var readOnlyValue = new Struct(42);
+
+ var subs = Substitute.For();
+ subs.When(x => x.Consume(Arg.Any())).Do(c => { c[0] = new Struct(24); });
+
+ subs.Consume(in readOnlyValue);
+
+ Assert.That(readOnlyValue.Value, Is.EqualTo(42));
+ }
+
+ [Test]
+ public void DelegateReadOnlyRefCannotBeModified()
+ {
+ var readOnlyValue = new Struct(42);
+
+ var subs = Substitute.For();
+ subs.When(x => x.Invoke(Arg.Any())).Do(c => { c[0] = new Struct(24); });
+
+ subs.Invoke(in readOnlyValue);
+
+ Assert.That(readOnlyValue.Value, Is.EqualTo(42));
+ }
+
+ [Test]
+ public void DelegateMultipleReadOnlyRefCannotBeModified()
+ {
+ var readOnlyValue1 = new Struct(42);
+ var readOnlyValue2 = new Struct(42);
+
+ var subs = Substitute.For();
+ subs.When(x => x.Invoke(Arg.Any(), Arg.Any()))
+ .Do(c => { c[0] = new Struct(24); c[1] = new Struct(24); });
+
+ subs.Invoke(in readOnlyValue1, in readOnlyValue2);
+
+ Assert.That(readOnlyValue1.Value, Is.EqualTo(42));
+ Assert.That(readOnlyValue2.Value, Is.EqualTo(42));
}
}
}
\ No newline at end of file