Repository: autofac/Autofac

Issue:
Inconsistent behavior of different RegistrationExtensions.WithProperty overloads regarding nullable properties
## Describe the Bug

The `WithProperty(Expression<Func<TLimit, TProperty>> propertyExpression, TProperty propertyValue)` throws when `propertyValue` is null, regardless whether `TProperty` is nullable. Nullable analysis does not flag an issue when passing `null`.
The relevant check is here: https://github.com/autofac/Autofac/blob/b222d0f635442dde23e219f14b2132932b028e81/src/Autofac/RegistrationExtensions.cs#L687-L690

All other `WithProperty` overloads accept `null` or `null!`.

## Steps to Reproduce

<!-- Tell us how to reproduce the issue. Ideally provide a failing unit test. -->

```c#
using Autofac;
using System;
using Xunit;

namespace Tests;

public class WithPropertyTests
{
    private class Test
    {
        public required int? Property { get; init; }
    }

    [Fact]
    public void WithProperty_NullPropertyValue()
    {
        var builder = new ContainerBuilder();
        builder.RegisterType<Test>()
            .WithProperty(nameof(Test.Property), null!)
            .AsSelf();

        builder.RegisterType<Test>()
            .WithProperty(new TypedParameter(typeof(int?), null))
            .AsSelf();

        Exception? exception = Record.Exception(() =>
            builder.RegisterType<Test>()
                .WithProperty(t => t.Property, null)
                .AsSelf());
        Assert.Null(exception);
    }
}
```

## Expected Behavior

I would expect `WithProperty(Expression<Func<TLimit, TProperty>> propertyExpression, TProperty propertyValue)` to accept a `null` `propertyValue` when `TProperty` is nullable, similar to the other overloads.

## Exception with Stack Trace

```text
Xunit.Sdk.NullException
Assert.Null() Failure: Value is not null
Expected: null
Actual:   System.ArgumentNullException: Value cannot be null. (Parameter 'propertyValue')
   at Autofac.RegistrationExtensions.WithProperty[TLimit,TReflectionActivatorData,TStyle,TProperty](IRegistrationBuilder`3 registration, Expression`1 propertyExpression, TProperty propertyValue)
   at Tests.WithPropertyTests.<>c__DisplayClass1_0.<WithProperty_NullPropertyValue>b__0() in […]\Tests\WithPropertyTests.cs:line 27
   at Xunit.Record.Exception(Func`1 testCode) in /_/src/xunit.core/Record.cs:line 47
   at Tests.WithPropertyTests.WithProperty_NullPropertyValue() in […]\Tests\WithPropertyTests.cs:line 30
   at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)
   at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)

```

## Dependency Versions

Autofac: 7.1.0

Inconsistent behavior of different RegistrationExtensions.WithProperty overloads regarding nullable properties
## Describe the Bug

The `WithProperty(Expression<Func<TLimit, TProperty>> propertyExpression, TProperty propertyValue)` throws when `propertyValue` is null, regardless whether `TProperty` is nullable. Nullable analysis does not flag an issue when passing `null`.
The relevant check is here: https://github.com/autofac/Autofac/blob/b222d0f635442dde23e219f14b2132932b028e81/src/Autofac/RegistrationExtensions.cs#L687-L690

All other `WithProperty` overloads accept `null` or `null!`.

## Steps to Reproduce

<!-- Tell us how to reproduce the issue. Ideally provide a failing unit test. -->

```c#
using Autofac;
using System;
using Xunit;

namespace Tests;

public class WithPropertyTests
{
    private class Test
    {
        public required int? Property { get; init; }
    }

    [Fact]
    public void WithProperty_NullPropertyValue()
    {
        var builder = new ContainerBuilder();
        builder.RegisterType<Test>()
            .WithProperty(nameof(Test.Property), null!)
            .AsSelf();

        builder.RegisterType<Test>()
            .WithProperty(new TypedParameter(typeof(int?), null))
            .AsSelf();

        Exception? exception = Record.Exception(() =>
            builder.RegisterType<Test>()
                .WithProperty(t => t.Property, null)
                .AsSelf());
        Assert.Null(exception);
    }
}
```

## Expected Behavior

I would expect `WithProperty(Expression<Func<TLimit, TProperty>> propertyExpression, TProperty propertyValue)` to accept a `null` `propertyValue` when `TProperty` is nullable, similar to the other overloads.

## Exception with Stack Trace

```text
Xunit.Sdk.NullException
Assert.Null() Failure: Value is not null
Expected: null
Actual:   System.ArgumentNullException: Value cannot be null. (Parameter 'propertyValue')
   at Autofac.RegistrationExtensions.WithProperty[TLimit,TReflectionActivatorData,TStyle,TProperty](IRegistrationBuilder`3 registration, Expression`1 propertyExpression, TProperty propertyValue)
   at Tests.WithPropertyTests.<>c__DisplayClass1_0.<WithProperty_NullPropertyValue>b__0() in […]\Tests\WithPropertyTests.cs:line 27
   at Xunit.Record.Exception(Func`1 testCode) in /_/src/xunit.core/Record.cs:line 47
   at Tests.WithPropertyTests.WithProperty_NullPropertyValue() in […]\Tests\WithPropertyTests.cs:line 30
   at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)
   at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)

```

## Dependency Versions

Autofac: 7.1.0



Hints:
This is sort of an interesting thing where I'm not entirely sure what the right answer is.

The reason I say that is that, generally speaking, we don't allow Autofac to "resolve null values." For example, you can't do this:

```csharp
builder.RegisterInstance<IService>(null);
```

And if you have property injection enabled on something and say `container.InjectProperties(obj);` then Autofac will never intentionally inject a null value - you'd get an exception instead.

On the other hand, as you point out, it doesn't look like things are consistent from a _parameter_ perspective - that we sometimes do allow null as a provided parameter and sometimes don't. And I agree, it should be consistent.

My question is - in which direction should parameters be made consistent? Should we stop null everywhere (why are folks intentionally injecting a null?) or should we allow null everywhere ("I want to intentionally set this one parameter to null to force a particular constructor/property behavior")?

On first glance it looks like most of the parameter mechanisms allow for null, which means making it consistently allow null for explicitly provided parameters would be the least breaking way to go, though it could be argued this is inconsistent with the nature of _resolved_ values since you can't resolve a null.

@alistairjevans what do you think?
I fully understand your concerns.

In our case, we have a class with various required properties.
We want some of these properties to be injected implicitly from other registrations and provide the missing properties using `WithProperty`.
The missing properties don’t make sense to be registered generally.
We have this issue with a nullable enum property.
I think if the majority of the `WithProperty` overloads let you pass null, they probably all should. 

I consider `WithProperty` to be relatively advanced, so if users have a usecase where they have a property they explicitly want nulled, I'm ok with it. That differs from a scenario where you say "Autofac, resolve and provide that value for me", in which case that can never be null.
Sounds good. I'll see if I can get a PR in to consistently allow null for this, unless someone beats me to it.
This is sort of an interesting thing where I'm not entirely sure what the right answer is.

The reason I say that is that, generally speaking, we don't allow Autofac to "resolve null values." For example, you can't do this:

```csharp
builder.RegisterInstance<IService>(null);
```

And if you have property injection enabled on something and say `container.InjectProperties(obj);` then Autofac will never intentionally inject a null value - you'd get an exception instead.

On the other hand, as you point out, it doesn't look like things are consistent from a _parameter_ perspective - that we sometimes do allow null as a provided parameter and sometimes don't. And I agree, it should be consistent.

My question is - in which direction should parameters be made consistent? Should we stop null everywhere (why are folks intentionally injecting a null?) or should we allow null everywhere ("I want to intentionally set this one parameter to null to force a particular constructor/property behavior")?

On first glance it looks like most of the parameter mechanisms allow for null, which means making it consistently allow null for explicitly provided parameters would be the least breaking way to go, though it could be argued this is inconsistent with the nature of _resolved_ values since you can't resolve a null.

@alistairjevans what do you think?
I fully understand your concerns.

In our case, we have a class with various required properties.
We want some of these properties to be injected implicitly from other registrations and provide the missing properties using `WithProperty`.
The missing properties don’t make sense to be registered generally.
We have this issue with a nullable enum property.
I think if the majority of the `WithProperty` overloads let you pass null, they probably all should. 

I consider `WithProperty` to be relatively advanced, so if users have a usecase where they have a property they explicitly want nulled, I'm ok with it. That differs from a scenario where you say "Autofac, resolve and provide that value for me", in which case that can never be null.
Sounds good. I'll see if I can get a PR in to consistently allow null for this, unless someone beats me to it.

Patch:
diff --git a/src/Autofac/Core/NamedPropertyParameter.cs b/src/Autofac/Core/NamedPropertyParameter.cs
index f94c74a7c..1723fad32 100644
--- a/src/Autofac/Core/NamedPropertyParameter.cs
+++ b/src/Autofac/Core/NamedPropertyParameter.cs
@@ -22,7 +22,7 @@ public class NamedPropertyParameter : ConstantParameter
     /// </summary>
     /// <param name="name">The name of the property.</param>
     /// <param name="value">The property value.</param>
-    public NamedPropertyParameter(string name, object value)
+    public NamedPropertyParameter(string name, object? value)
         : base(value, pi =>
         {
             return pi.TryGetDeclaringProperty(out PropertyInfo? prop) &&
diff --git a/src/Autofac/NamedParameter.cs b/src/Autofac/NamedParameter.cs
index 421301d1d..e8b38eeee 100644
--- a/src/Autofac/NamedParameter.cs
+++ b/src/Autofac/NamedParameter.cs
@@ -45,7 +45,7 @@ public class NamedParameter : ConstantParameter
     /// </summary>
     /// <param name="name">The name of the parameter.</param>
     /// <param name="value">The parameter value.</param>
-    public NamedParameter(string name, object value)
+    public NamedParameter(string name, object? value)
         : base(value, pi => pi.Name == name) =>
             Name = Enforce.ArgumentNotNullOrEmpty(name, "name");
 }
diff --git a/src/Autofac/PositionalParameter.cs b/src/Autofac/PositionalParameter.cs
index 92be04335..ce4db4c34 100644
--- a/src/Autofac/PositionalParameter.cs
+++ b/src/Autofac/PositionalParameter.cs
@@ -46,7 +46,7 @@ public class PositionalParameter : ConstantParameter
     /// </summary>
     /// <param name="position">The zero-based position of the parameter.</param>
     /// <param name="value">The parameter value.</param>
-    public PositionalParameter(int position, object value)
+    public PositionalParameter(int position, object? value)
         : base(value, pi => pi.Position == position && (pi.Member is ConstructorInfo))
     {
         if (position < 0)
diff --git a/src/Autofac/RegistrationExtensions.cs b/src/Autofac/RegistrationExtensions.cs
index 18a2c1c1b..515a8d352 100644
--- a/src/Autofac/RegistrationExtensions.cs
+++ b/src/Autofac/RegistrationExtensions.cs
@@ -621,7 +621,7 @@ public static IRegistrationBuilder<TLimit, TReflectionActivatorData, TStyle>
         WithProperty<TLimit, TReflectionActivatorData, TStyle>(
             this IRegistrationBuilder<TLimit, TReflectionActivatorData, TStyle> registration,
             string propertyName,
-            object propertyValue)
+            object? propertyValue)
         where TReflectionActivatorData : ReflectionActivatorData
     {
         return registration.WithProperty(new NamedPropertyParameter(propertyName, propertyValue));
@@ -684,11 +684,6 @@ public static IRegistrationBuilder<TLimit, TReflectionActivatorData, TStyle>
             throw new ArgumentNullException(nameof(propertyExpression));
         }
 
-        if (propertyValue == null)
-        {
-            throw new ArgumentNullException(nameof(propertyValue));
-        }
-
         var propertyInfo = (propertyExpression.Body as MemberExpression)?.Member as PropertyInfo ?? throw new ArgumentOutOfRangeException(nameof(propertyExpression), RegistrationExtensionsResources.ExpressionDoesNotReferToProperty);
         return registration.WithProperty(new NamedPropertyParameter(propertyInfo.Name, propertyValue));
     }


Test patch:
diff --git a/test/Autofac.Specification.Test/Features/PropertyInjectionTests.cs b/test/Autofac.Specification.Test/Features/PropertyInjectionTests.cs
index 0165a495f..b95b74bc8 100644
--- a/test/Autofac.Specification.Test/Features/PropertyInjectionTests.cs
+++ b/test/Autofac.Specification.Test/Features/PropertyInjectionTests.cs
@@ -398,6 +398,39 @@ public void DecoratedInstanceWithPropertyInjectionAllowingCircularReferencesStil
         instance.AssertProp();
     }
 
+    [Fact]
+    public void WithPropertyDelegateAllowsNullValue()
+    {
+        // Issue 1427: WithProperty should consistently allow null values.
+        var builder = new ContainerBuilder();
+        builder.RegisterType<HasPublicSetter>().WithProperty(t => t.Val, null);
+        var container = builder.Build();
+        var instance = container.Resolve<HasPublicSetter>();
+        Assert.Null(instance.Val);
+    }
+
+    [Fact]
+    public void WithPropertyNamedAllowsNullValue()
+    {
+        // Issue 1427: WithProperty should consistently allow null values.
+        var builder = new ContainerBuilder();
+        builder.RegisterType<HasPublicSetter>().WithProperty(nameof(HasPublicSetter.Val), null);
+        var container = builder.Build();
+        var instance = container.Resolve<HasPublicSetter>();
+        Assert.Null(instance.Val);
+    }
+
+    [Fact]
+    public void WithPropertyTypedAllowsNullValue()
+    {
+        // Issue 1427: WithProperty should consistently allow null values.
+        var builder = new ContainerBuilder();
+        builder.RegisterType<HasPublicSetter>().WithProperty(TypedParameter.From<string>(null));
+        var container = builder.Build();
+        var instance = container.Resolve<HasPublicSetter>();
+        Assert.Null(instance.Val);
+    }
+
     private class ConstructorParamNotAttachedToProperty
     {
         [SuppressMessage("SA1401", "SA1401")]