Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .nuget/nuget.exe
Binary file not shown.
7 changes: 7 additions & 0 deletions Source/Csla/DataPortalOperationAttributes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ public class InjectAttribute : Attribute
/// service is not registered.
/// </summary>
public bool AllowNull { get; set; }

/// <summary>
/// Gets or sets the key used to identify a keyed service registration.
/// When specified, the service is retrieved using GetKeyedService or GetRequiredKeyedService.
/// Requires .NET 8.0 or higher.
/// </summary>
public object? Key { get; set; }
}

/// <summary>
Expand Down
39 changes: 34 additions & 5 deletions Source/Csla/Reflection/ServiceProviderMethodCaller.cs
Original file line number Diff line number Diff line change
Expand Up @@ -553,11 +553,40 @@ private static ParameterInfo[] GetDIParameters(System.Reflection.MethodInfo meth
{
throw new NullReferenceException(nameof(service));
}
// Use GetService for optional (allows null) or GetRequiredService for required (throws if not registered)
plist[index] = method.AllowNull[index]
? service.GetService(item.ParameterType)
: service.GetRequiredService(item.ParameterType);


var serviceKey = method.ServiceKeys[index];
if (serviceKey != null)
{
#if NET8_0_OR_GREATER
// Use keyed service injection for .NET 8+
if (method.AllowNull[index])
{
// For optional keyed services, cast to IKeyedServiceProvider
if (service is IKeyedServiceProvider keyedProvider)
{
plist[index] = keyedProvider.GetKeyedService(item.ParameterType, serviceKey);
}
else
{
throw new InvalidOperationException("Service provider must implement IKeyedServiceProvider to support keyed services.");
}
}
else
{
// For required keyed services, use extension method
plist[index] = service.GetRequiredKeyedService(item.ParameterType, serviceKey);
}
#else
throw new NotSupportedException("Keyed service injection is only supported on .NET 8.0 or higher.");
#endif
}
else
{
// Use GetService for optional (allows null) or GetRequiredService for required (throws if not registered)
plist[index] = method.AllowNull[index]
? service.GetService(item.ParameterType)
: service.GetRequiredService(item.ParameterType);
}
}
else
{
Expand Down
11 changes: 9 additions & 2 deletions Source/Csla/Reflection/ServiceProviderMethodInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ namespace Csla.Reflection
/// </summary>
public class ServiceProviderMethodInfo
{
[MemberNotNullWhen(true, nameof(DynamicMethod), nameof(Parameters), nameof(IsInjected), nameof(AllowNull), nameof(DataPortalMethodInfo))]
[MemberNotNullWhen(true, nameof(DynamicMethod), nameof(Parameters), nameof(IsInjected), nameof(AllowNull), nameof(ServiceKeys), nameof(DataPortalMethodInfo))]
private bool Initialized { get; set; }

/// <summary>
Expand Down Expand Up @@ -55,6 +55,11 @@ public class ServiceProviderMethodInfo
/// </summary>
public bool[]? AllowNull { get; private set; }
/// <summary>
/// Gets an array of keys for injected parameters.
/// Null entries indicate non-keyed services.
/// </summary>
public object?[]? ServiceKeys { get; private set; }
/// <summary>
/// Gets a value indicating whether the method
/// returns type Task
/// </summary>
Expand Down Expand Up @@ -83,7 +88,7 @@ public ServiceProviderMethodInfo(System.Reflection.MethodInfo methodInfo)
/// Initializes and caches the metastate values
/// necessary to invoke the method
/// </summary>
[MemberNotNull(nameof(DynamicMethod), nameof(Parameters), nameof(IsInjected), nameof(AllowNull), nameof(DataPortalMethodInfo))]
[MemberNotNull(nameof(DynamicMethod), nameof(Parameters), nameof(IsInjected), nameof(AllowNull), nameof(ServiceKeys), nameof(DataPortalMethodInfo))]
public void PrepForInvocation()
{
if (!Initialized)
Expand All @@ -97,6 +102,7 @@ public void PrepForInvocation()
TakesParamArray = (Parameters.Length == 1 && Parameters[0].ParameterType.Equals(typeof(object[])));
IsInjected = new bool[Parameters.Length];
AllowNull = new bool[Parameters.Length];
ServiceKeys = new object?[Parameters.Length];

int index = 0;
foreach (var item in Parameters)
Expand All @@ -106,6 +112,7 @@ public void PrepForInvocation()
{
IsInjected[index] = true;
AllowNull[index] = injectAttribute.AllowNull || ParameterAllowsNull(item);
ServiceKeys[index] = injectAttribute.Key;
}
index++;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,92 @@
await FluentActions.Invoking(async () => await portal.CreateAsync())
.Should().ThrowAsync<Exception>();
}

#if NET8_0_OR_GREATER
[TestMethod]
public void FindMethodWithKeyedServiceInjection()
{
var obj = new KeyedServiceInjection();
var method = _systemUnderTest.FindDataPortalMethod<CreateAttribute>(obj, null);

method.Should().NotBeNull();
method.PrepForInvocation();
method.Parameters.Should().HaveCount(1);
method.IsInjected.Should().HaveCount(1);
method.IsInjected![0].Should().BeTrue();
method.ServiceKeys.Should().HaveCount(1);
method.ServiceKeys![0].Should().Be("serviceA");
}

[TestMethod]
public async Task InvokeMethodWithKeyedServiceInjection()
{
var contextWithService = TestDIContextFactory.CreateDefaultContext(services =>
{
services.AddKeyedTransient<IOptionalService, ServiceAImplementation>("serviceA");
});

var portal = contextWithService.CreateDataPortal<KeyedServiceInjection>();
var obj = await portal.CreateAsync();

obj.Should().NotBeNull();
obj.Data.Should().Be("Service A data");
}

[TestMethod]
public void FindMethodWithKeyedServiceAndCriteriaInjection()
{
var obj = new KeyedServiceWithCriteriaInjection();
var method = _systemUnderTest.FindDataPortalMethod<CreateAttribute>(obj, [123]);

method.Should().NotBeNull();
method.PrepForInvocation();
method.Parameters.Should().HaveCount(2);
method.IsInjected.Should().HaveCount(2);
method.IsInjected![0].Should().BeFalse(); // First param is criteria
method.IsInjected![1].Should().BeTrue(); // Second param is injected
method.ServiceKeys.Should().HaveCount(2);
method.ServiceKeys![0].Should().BeNull(); // First param is not injected
method.ServiceKeys![1].Should().Be("serviceB");
}

[TestMethod]
public async Task InvokeMethodWithKeyedServiceAndCriteriaInjection()
{
var contextWithService = TestDIContextFactory.CreateDefaultContext(services =>
{
services.AddKeyedTransient<IOptionalService, ServiceBImplementation>("serviceB");
});

var portal = contextWithService.CreateDataPortal<KeyedServiceWithCriteriaInjection>();
var obj = await portal.CreateAsync(42);

obj.Should().NotBeNull();
obj.Id.Should().Be(42);
obj.Data.Should().Be("Service B data");
}

[TestMethod]
public async Task InvokeMethodWithOptionalKeyedServiceInjection_ServiceNotRegistered()
{
var portal = _diContext.CreateDataPortal<OptionalKeyedServiceInjection>();
var obj = await portal.CreateAsync();

obj.Should().NotBeNull();
obj.Data.Should().Be("Keyed service is null as expected");
}

[TestMethod]
public async Task InvokeMethodWithKeyedServiceInjection_ThrowsWhenServiceNotRegistered()
{
var portal = _diContext.CreateDataPortal<KeyedServiceInjection>();

// This should throw because the keyed service is required but not registered
// The exception will be wrapped in DataPortalException
await FluentActions.Invoking(async () => await portal.CreateAsync())
.Should().ThrowAsync<Exception>();
}
#endif
}

#region Classes for testing various scenarios of loading/finding data portal methods
Expand Down Expand Up @@ -787,7 +873,7 @@
{
}

public class Criteria : Csla.CriteriaBase<Criteria>

Check warning on line 876 in Source/tests/csla.netcore.test/DataPortal/ServiceProviderMethodCallerTests.cs

View workflow job for this annotation

GitHub Actions / build

'CriteriaBase<Issue2287ListBase.Criteria>' is obsolete: 'Use types that can be serialized by CSLA. See the `/docs/Upgrade to CSLA 9.md` document for details.'
{
}
}
Expand Down Expand Up @@ -920,5 +1006,83 @@
}
}

#if NET8_0_OR_GREATER
// Tests for keyed service injection (NET8+ only)
public class KeyedServiceInjection : BusinessBase<KeyedServiceInjection>
{
public static readonly PropertyInfo<string> DataProperty = RegisterProperty<string>(nameof(Data));
public string Data
{
get => GetProperty(DataProperty);
set => SetProperty(DataProperty, value);
}

[Create]
private void Create([Inject(Key = "serviceA")] IOptionalService keyedService)
{
using (BypassPropertyChecks)
{
Data = keyedService.GetData();
}
}
}

public class KeyedServiceWithCriteriaInjection : BusinessBase<KeyedServiceWithCriteriaInjection>
{
public static readonly PropertyInfo<string> DataProperty = RegisterProperty<string>(nameof(Data));
public string Data
{
get => GetProperty(DataProperty);
set => SetProperty(DataProperty, value);
}

public static readonly PropertyInfo<int> IdProperty = RegisterProperty<int>(nameof(Id));
public int Id
{
get => GetProperty(IdProperty);
set => SetProperty(IdProperty, value);
}

[Create]
private void Create(int id, [Inject(Key = "serviceB")] IOptionalService keyedService)
{
using (BypassPropertyChecks)
{
Id = id;
Data = keyedService.GetData();
}
}
}

public class OptionalKeyedServiceInjection : BusinessBase<OptionalKeyedServiceInjection>
{
public static readonly PropertyInfo<string> DataProperty = RegisterProperty<string>(nameof(Data));
public string Data
{
get => GetProperty(DataProperty);
set => SetProperty(DataProperty, value);
}

[Create]
private void Create([Inject(Key = "nonExistent", AllowNull = true)] IOptionalService? keyedService)
{
using (BypassPropertyChecks)
{
Data = keyedService == null ? "Keyed service is null as expected" : keyedService.GetData();
}
}
}

public class ServiceAImplementation : IOptionalService
{
public string GetData() => "Service A data";
}

public class ServiceBImplementation : IOptionalService
{
public string GetData() => "Service B data";
}
#endif

#endregion
}
Loading