diff --git a/Source/Csla/Reflection/ServiceProviderMethodCaller.cs b/Source/Csla/Reflection/ServiceProviderMethodCaller.cs index 36f4828678..937fe2179a 100644 --- a/Source/Csla/Reflection/ServiceProviderMethodCaller.cs +++ b/Source/Csla/Reflection/ServiceProviderMethodCaller.cs @@ -123,7 +123,18 @@ public bool TryFindDataPortalMethod([DynamicallyAccessedMembers(DynamicallyAc var typeOfOperation = typeof(T); - var cacheKey = GetCacheKeyName(targetType, typeOfOperation, criteria, useLegacyMethods); + // Resolve the factory type (if any) up front so it can participate in the cache key. + // In production a business type maps to exactly one factory, but a custom + // IObjectFactoryLoader can resolve the same FactoryTypeName to different types. The + // method cache is process-wide and keyed by business type, so the resolved factory + // type must be part of the key to avoid invoking a delegate compiled for a different + // factory. + var factoryInfo = ObjectFactoryAttribute.GetObjectFactoryAttribute(targetType); + Type? factoryType = null; + if (factoryInfo != null && !TryGetFactoryType(factoryInfo, _applicationContext, throwOnError, out factoryType)) + return null; + + var cacheKey = GetCacheKeyName(targetType, typeOfOperation, criteria, useLegacyMethods, factoryType); #if NET8_0_OR_GREATER if (_methodCache.TryGetValue(cacheKey, out var unloadableCachedMethodInfo)) @@ -139,32 +150,27 @@ public bool TryFindDataPortalMethod([DynamicallyAccessedMembers(DynamicallyAc } var candidates = new List(); - var factoryInfo = ObjectFactoryAttribute.GetObjectFactoryAttribute(targetType); if (factoryInfo != null) { - if (!TryGetFactoryType(factoryInfo, _applicationContext, throwOnError, out var factoryType)) - { - return null; - } - + var factoryWalkType = factoryType; var ftList = new List(); var level = 0; - while (factoryType != null) + while (factoryWalkType != null) { ftList.Clear(); if (typeOfOperation == typeof(CreateAttribute)) - ftList.AddRange(factoryType.GetMethods(_factoryBindingAttr).Where(m => m.Name == factoryInfo.CreateMethodName)); + ftList.AddRange(factoryWalkType.GetMethods(_factoryBindingAttr).Where(m => m.Name == factoryInfo.CreateMethodName)); else if (typeOfOperation == typeof(FetchAttribute)) - ftList.AddRange(factoryType.GetMethods(_factoryBindingAttr).Where(m => m.Name == factoryInfo.FetchMethodName)); + ftList.AddRange(factoryWalkType.GetMethods(_factoryBindingAttr).Where(m => m.Name == factoryInfo.FetchMethodName)); else if (typeOfOperation == typeof(DeleteAttribute)) - ftList.AddRange(factoryType.GetMethods(_factoryBindingAttr).Where(m => m.Name == factoryInfo.DeleteMethodName)); + ftList.AddRange(factoryWalkType.GetMethods(_factoryBindingAttr).Where(m => m.Name == factoryInfo.DeleteMethodName)); else if (typeOfOperation == typeof(ExecuteAttribute)) - ftList.AddRange(factoryType.GetMethods(_factoryBindingAttr).Where(m => m.Name == factoryInfo.ExecuteMethodName)); + ftList.AddRange(factoryWalkType.GetMethods(_factoryBindingAttr).Where(m => m.Name == factoryInfo.ExecuteMethodName)); else if (typeOfOperation == typeof(CreateChildAttribute)) - ftList.AddRange(factoryType.GetMethods(_factoryBindingAttr).Where(m => m.Name == "Child_Create")); + ftList.AddRange(factoryWalkType.GetMethods(_factoryBindingAttr).Where(m => m.Name == "Child_Create")); else - ftList.AddRange(factoryType.GetMethods(_factoryBindingAttr).Where(m => m.Name == factoryInfo.UpdateMethodName)); - factoryType = factoryType.BaseType; + ftList.AddRange(factoryWalkType.GetMethods(_factoryBindingAttr).Where(m => m.Name == factoryInfo.UpdateMethodName)); + factoryWalkType = factoryWalkType.BaseType; candidates.AddRange(ftList.Select(r => new ScoredMethodInfo { MethodInfo = r, Score = level })); level--; } @@ -173,22 +179,6 @@ public bool TryFindDataPortalMethod([DynamicallyAccessedMembers(DynamicallyAc var ftlist = targetType.GetMethods(_bindingAttr).Where(m => m.Name == "Child_Create"); candidates.AddRange(ftlist.Select(r => new ScoredMethodInfo { MethodInfo = r, Score = 0 })); } - - static bool TryGetFactoryType(ObjectFactoryAttribute factoryAttribute, ApplicationContext context, bool throwOnError, [NotNullWhen(true)] out Type? factoryType) - { - try - { - var factoryLoader = context.CurrentServiceProvider.GetRequiredService(); - factoryType = factoryLoader.GetFactoryType(factoryAttribute.FactoryTypeName); - } - catch when (!throwOnError) - { - factoryType = null; - return false; - } - - return factoryType is not null; - } } else // not using factory types { @@ -225,6 +215,22 @@ static bool TryGetFactoryType(ObjectFactoryAttribute factoryAttribute, Applicati } } + static bool TryGetFactoryType(ObjectFactoryAttribute factoryAttribute, ApplicationContext context, bool throwOnError, [NotNullWhen(true)] out Type? factoryType) + { + try + { + var factoryLoader = context.CurrentServiceProvider.GetRequiredService(); + factoryType = factoryLoader.GetFactoryType(factoryAttribute.FactoryTypeName); + } + catch when (!throwOnError) + { + factoryType = null; + return false; + } + + return factoryType is not null; + } + ScoredMethodInfo? result = null; if (candidates.Any()) @@ -431,10 +437,11 @@ private static int CalculateParameterScore(ParameterInfo methodParam, object? c) return 0; } - private static string GetCacheKeyName(Type targetType, Type operationType, object?[]? criteria, bool useLegacyMethods) + private static string GetCacheKeyName(Type targetType, Type operationType, object?[]? criteria, bool useLegacyMethods, Type? factoryType = null) { var legacy = useLegacyMethods ? "" : "|nolegacy"; - return $"{targetType.FullName}.[{operationType.Name.Replace("Attribute", "")}]{GetCriteriaTypeNames(criteria)}{legacy}"; + var factory = factoryType is null ? "" : $"|{factoryType.FullName}"; + return $"{targetType.FullName}.[{operationType.Name.Replace("Attribute", "")}]{GetCriteriaTypeNames(criteria)}{legacy}{factory}"; } private static string GetCriteriaTypeNames(object?[]? criteria) @@ -619,7 +626,11 @@ private static ParameterInfo[] GetDIParameters(System.Reflection.MethodInfo meth } else if (method.IsAsyncTaskObject) { - return await ((Task)method.DynamicMethod!(obj, plist)).ConfigureAwait(false); + // The method returns Task; Task is invariant so it cannot be cast to + // Task. Use the conversion helper to await it and extract the result. + var returnValue = method.DynamicMethod!(obj, plist); + var convertedTask = (Task)method.ConvertToTaskObjectMethod!.Invoke(null, [returnValue])!; + return await convertedTask.ConfigureAwait(false); } else { diff --git a/Source/Csla/Reflection/ServiceProviderMethodInfo.cs b/Source/Csla/Reflection/ServiceProviderMethodInfo.cs index 5595201d38..25ec5729ea 100644 --- a/Source/Csla/Reflection/ServiceProviderMethodInfo.cs +++ b/Source/Csla/Reflection/ServiceProviderMethodInfo.cs @@ -70,6 +70,11 @@ public class ServiceProviderMethodInfo /// public bool IsAsyncTaskObject { get; set; } /// + /// Gets the helper method used to convert a Task of T + /// return value into a Task of object + /// + internal System.Reflection.MethodInfo? ConvertToTaskObjectMethod { get; private set; } + /// /// Gets the DataPortalInfo for the method /// internal DataPortalMethodInfo? DataPortalMethodInfo { get; private set; } @@ -118,6 +123,8 @@ public void PrepForInvocation() } IsAsyncTask = (MethodInfo.ReturnType == typeof(Task)); IsAsyncTaskObject = (MethodInfo.ReturnType.IsGenericType && (MethodInfo.ReturnType.GetGenericTypeDefinition() == typeof(Task<>))); + if (IsAsyncTaskObject) + ConvertToTaskObjectMethod = TaskConversionHelper.CreateTaskObjectConversionMethodInfo(MethodInfo.ReturnType.GetGenericArguments()[0]); DataPortalMethodInfo = new DataPortalMethodInfo(MethodInfo); Initialized = true; diff --git a/Source/Csla/Server/FactoryDataPortal.cs b/Source/Csla/Server/FactoryDataPortal.cs index 11faa6abe5..408f1a7749 100644 --- a/Source/Csla/Server/FactoryDataPortal.cs +++ b/Source/Csla/Server/FactoryDataPortal.cs @@ -24,6 +24,9 @@ public class FactoryDataPortal : IDataPortalServer private readonly IObjectFactoryLoader _factoryLoader; private readonly IDataPortalExceptionInspector _exceptionInspector; private readonly DataPortalOptions _dataPortalOptions; + private Reflection.ServiceProviderMethodCaller? _serviceProviderMethodCaller; + private Reflection.ServiceProviderMethodCaller ServiceProviderMethodCaller => + _serviceProviderMethodCaller ??= _applicationContext.CreateInstanceDI(); /// /// Creates an instance of the type. @@ -43,18 +46,39 @@ public FactoryDataPortal(ApplicationContext applicationContext, IObjectFactoryLo #region Method invokes - private async Task InvokeMethod(string factoryTypeName, DataPortalOperations operation, string methodName, Type objectType, DataPortalContext context, bool isSync) + private async Task InvokeMethod(string factoryTypeName, DataPortalOperations operation, string methodName, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type objectType, object? e, DataPortalContext context, bool isSync) + where T : DataPortalOperationAttribute { object factory = _factoryLoader.GetFactory(factoryTypeName); - var eventArgs = new DataPortalEventArgs(context, objectType, null, operation); + // EmptyCriteria is an internal marker for "no criteria"; surface it to the + // factory lifecycle hooks as null, matching the historical behavior. + var eventArg = e is EmptyCriteria ? null : e; + var eventArgs = new DataPortalEventArgs(context, objectType, eventArg, operation); Reflection.MethodCaller.CallMethodIfImplemented(factory, "Invoke", eventArgs); object? result; try { - Utilities.ThrowIfAsyncMethodOnSyncClient(_applicationContext, isSync, factory, methodName); + var criteria = DataPortal.GetCriteriaArray(e); + if (ServiceProviderMethodCaller.TryFindDataPortalMethod(objectType, criteria, out var method)) + { + // DI-aware path: supports multiple criteria parameters and method-level [Inject] + Utilities.ThrowIfAsyncMethodOnSyncClient(_applicationContext, isSync, method.MethodInfo); + result = await ServiceProviderMethodCaller.CallMethodTryAsync(factory, method, criteria).ConfigureAwait(false); + } + else if (e is null or EmptyCriteria) + { + // legacy fallback: parameterless factory method + Utilities.ThrowIfAsyncMethodOnSyncClient(_applicationContext, isSync, factory, methodName); + result = await Reflection.MethodCaller.CallMethodTryAsync(factory, methodName).ConfigureAwait(false); + } + else + { + // legacy fallback: single criteria object + Utilities.ThrowIfAsyncMethodOnSyncClient(_applicationContext, isSync, factory, methodName, e); + result = await Reflection.MethodCaller.CallMethodTryAsync(factory, methodName, e).ConfigureAwait(false); + } - result = await Reflection.MethodCaller.CallMethodTryAsync(factory, methodName).ConfigureAwait(false); if (result is Exception error) throw error; @@ -66,35 +90,7 @@ private async Task InvokeMethod(string factoryTypeName, DataPo catch (Exception ex) { Reflection.MethodCaller.CallMethodIfImplemented( - factory, "InvokeError", new DataPortalEventArgs(context, objectType, null, operation, ex)); - throw; - } - return new DataPortalResult(_applicationContext, result, null); - } - - private async Task InvokeMethod(string factoryTypeName, DataPortalOperations operation, string methodName, Type objectType, object e, DataPortalContext context, bool isSync) - { - object factory = _factoryLoader.GetFactory(factoryTypeName); - var eventArgs = new DataPortalEventArgs(context, objectType, e, operation); - - Reflection.MethodCaller.CallMethodIfImplemented(factory, "Invoke", eventArgs); - object? result; - try - { - Utilities.ThrowIfAsyncMethodOnSyncClient(_applicationContext, isSync, factory, methodName, e); - - result = await Reflection.MethodCaller.CallMethodTryAsync(factory, methodName, e).ConfigureAwait(false); - if (result is Exception error) - throw error; - - if (result is Core.ITrackStatus busy && busy.IsBusy) - throw new InvalidOperationException($"{objectType.Name}.IsBusy == true"); - - Reflection.MethodCaller.CallMethodIfImplemented(factory, "InvokeComplete", eventArgs); - } - catch (Exception ex) - { - Reflection.MethodCaller.CallMethodIfImplemented(factory, "InvokeError", new DataPortalEventArgs(context, objectType, e, operation, ex)); + factory, "InvokeError", new DataPortalEventArgs(context, objectType, eventArg, operation, ex)); throw; } return new DataPortalResult(_applicationContext, result, null); @@ -118,11 +114,7 @@ public async Task Create([DynamicallyAccessedMembers(Dynamical try { - DataPortalResult result; - if (criteria is EmptyCriteria) - result = await InvokeMethod(context.FactoryInfo.FactoryTypeName, DataPortalOperations.Create, context.FactoryInfo.CreateMethodName, objectType, context, isSync).ConfigureAwait(false); - else - result = await InvokeMethod(context.FactoryInfo.FactoryTypeName, DataPortalOperations.Create, context.FactoryInfo.CreateMethodName, objectType, criteria, context, isSync).ConfigureAwait(false); + var result = await InvokeMethod(context.FactoryInfo.FactoryTypeName, DataPortalOperations.Create, context.FactoryInfo.CreateMethodName, objectType, criteria, context, isSync).ConfigureAwait(false); return result; } catch (Exception ex) @@ -153,11 +145,7 @@ public async Task Fetch([DynamicallyAccessedMembers(Dynamicall try { - DataPortalResult result; - if (criteria is EmptyCriteria) - result = await InvokeMethod(context.FactoryInfo.FactoryTypeName, DataPortalOperations.Fetch, context.FactoryInfo.FetchMethodName, objectType, context, isSync).ConfigureAwait(false); - else - result = await InvokeMethod(context.FactoryInfo.FactoryTypeName, DataPortalOperations.Fetch, context.FactoryInfo.FetchMethodName, objectType, criteria, context, isSync).ConfigureAwait(false); + var result = await InvokeMethod(context.FactoryInfo.FactoryTypeName, DataPortalOperations.Fetch, context.FactoryInfo.FetchMethodName, objectType, criteria, context, isSync).ConfigureAwait(false); return result; } catch (Exception ex) @@ -169,15 +157,11 @@ public async Task Fetch([DynamicallyAccessedMembers(Dynamicall } } - private async Task Execute(Type objectType, object criteria, DataPortalContext context, bool isSync, ObjectFactoryAttribute factoryInfo) + private async Task Execute([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type objectType, object criteria, DataPortalContext context, bool isSync, ObjectFactoryAttribute factoryInfo) { try { - DataPortalResult result; - if (criteria is EmptyCriteria) - result = await InvokeMethod(factoryInfo.FactoryTypeName, DataPortalOperations.Execute, factoryInfo.ExecuteMethodName, objectType, context, isSync).ConfigureAwait(false); - else - result = await InvokeMethod(factoryInfo.FactoryTypeName, DataPortalOperations.Execute, factoryInfo.ExecuteMethodName, objectType, criteria, context, isSync).ConfigureAwait(false); + var result = await InvokeMethod(factoryInfo.FactoryTypeName, DataPortalOperations.Execute, factoryInfo.ExecuteMethodName, objectType, criteria, context, isSync).ConfigureAwait(false); return result; } catch (Exception ex) @@ -205,11 +189,15 @@ public async Task Update(ICslaObject obj, DataPortalContext co { DataPortalResult result; if (obj is Core.ICommandObject) + { methodName = context.FactoryInfo.ExecuteMethodName; + result = await InvokeMethod(context.FactoryInfo.FactoryTypeName, DataPortalOperations.Update, methodName, obj.GetType(), obj, context, isSync).ConfigureAwait(false); + } else + { methodName = context.FactoryInfo.UpdateMethodName; - - result = await InvokeMethod(context.FactoryInfo.FactoryTypeName, DataPortalOperations.Update, methodName, obj.GetType(), obj, context, isSync).ConfigureAwait(false); + result = await InvokeMethod(context.FactoryInfo.FactoryTypeName, DataPortalOperations.Update, methodName, obj.GetType(), obj, context, isSync).ConfigureAwait(false); + } return result; } catch (Exception ex) @@ -236,11 +224,7 @@ public async Task Delete([DynamicallyAccessedMembers(Dynamical try { - DataPortalResult result; - if (criteria is EmptyCriteria) - result = await InvokeMethod(context.FactoryInfo.FactoryTypeName, DataPortalOperations.Delete, context.FactoryInfo.DeleteMethodName, objectType, context, isSync).ConfigureAwait(false); - else - result = await InvokeMethod(context.FactoryInfo.FactoryTypeName, DataPortalOperations.Delete, context.FactoryInfo.DeleteMethodName, objectType, criteria, context, isSync).ConfigureAwait(false); + var result = await InvokeMethod(context.FactoryInfo.FactoryTypeName, DataPortalOperations.Delete, context.FactoryInfo.DeleteMethodName, objectType, criteria, context, isSync).ConfigureAwait(false); return result; } catch (Exception ex) diff --git a/Source/tests/Csla.test/ObjectFactory/InjectCommandObject.cs b/Source/tests/Csla.test/ObjectFactory/InjectCommandObject.cs new file mode 100644 index 0000000000..b7a43e4fdf --- /dev/null +++ b/Source/tests/Csla.test/ObjectFactory/InjectCommandObject.cs @@ -0,0 +1,47 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Marimer LLC. All rights reserved. +// Website: https://cslanet.com +// +// Command object whose factory Execute method uses [Inject] +//----------------------------------------------------------------------- + +namespace Csla.Test.ObjectFactory +{ + [Server.ObjectFactory("Csla.Test.ObjectFactory.InjectCommandObjectFactory, Csla.Tests")] + [Serializable] + public class InjectCommandObject : CommandBase + { + public static readonly PropertyInfo ValueProperty = RegisterProperty(p => p.Value); + public string Value + { + get => ReadProperty(ValueProperty); + set => LoadProperty(ValueProperty, value); + } + + public static InjectCommandObject Execute(IDataPortal dataPortal) + { + var cmd = dataPortal.Create(); + return dataPortal.Execute(cmd); + } + } + + public class InjectCommandObjectFactory : Csla.Server.ObjectFactory + { + public InjectCommandObjectFactory(ApplicationContext applicationContext) : base(applicationContext) + { + } + + [RunLocal] + public object Create() + { + return ApplicationContext.CreateInstanceDI(); + } + + public object Execute(InjectCommandObject command, [Inject] IFactoryTestService service) + { + command.Value = service.GetValue(); + return command; + } + } +} diff --git a/Source/tests/Csla.test/ObjectFactory/MultiParamInjectFactories.cs b/Source/tests/Csla.test/ObjectFactory/MultiParamInjectFactories.cs new file mode 100644 index 0000000000..28f22620f1 --- /dev/null +++ b/Source/tests/Csla.test/ObjectFactory/MultiParamInjectFactories.cs @@ -0,0 +1,130 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Marimer LLC. All rights reserved. +// Website: https://cslanet.com +// +// Factory fixtures exercising multiple criteria parameters +//----------------------------------------------------------------------- + +namespace Csla.Test.ObjectFactory +{ + /// + /// Simple service used to verify dependency injection into + /// object factory methods and constructors. + /// + public interface IFactoryTestService + { + string GetValue(); + } + + public class FactoryTestService : IFactoryTestService + { + public string GetValue() => "injected"; + } + + /// + /// Factory whose create/fetch methods accept multiple criteria parameters. + /// + public class MultiParamRootFactory : Csla.Server.ObjectFactory + { + public MultiParamRootFactory(ApplicationContext applicationContext) + : base(applicationContext) { } + + public object Create(string text, int number) + { + var obj = ApplicationContext.CreateInstanceDI(); + obj.Data = $"Create {text} {number}"; + obj.MarkAsNew(); + return obj; + } + + public object Fetch(string text, int number) + { + var obj = ApplicationContext.CreateInstanceDI(); + obj.Data = $"Fetch {text} {number}"; + obj.MarkAsOld(); + return obj; + } + } + + /// + /// Factory whose methods mix criteria parameters with [Inject] parameters. + /// + public class InjectRootFactory : Csla.Server.ObjectFactory + { + public InjectRootFactory(ApplicationContext applicationContext) + : base(applicationContext) { } + + public object Fetch(string id, [Inject] IFactoryTestService service) + { + var obj = ApplicationContext.CreateInstanceDI(); + obj.Data = $"{id}:{service.GetValue()}"; + obj.MarkAsOld(); + return obj; + } + + public object Create(string text, int number, [Inject] IFactoryTestService service) + { + var obj = ApplicationContext.CreateInstanceDI(); + obj.Data = $"{text} {number} {service.GetValue()}"; + obj.MarkAsNew(); + return obj; + } + } + + /// + /// Factory that receives its dependency through constructor injection. + /// + public class CtorInjectRootFactory : Csla.Server.ObjectFactory + { + private readonly IFactoryTestService _service; + + public CtorInjectRootFactory(ApplicationContext applicationContext, IFactoryTestService service) + : base(applicationContext) + { + _service = service; + } + + public object Fetch() + { + var obj = ApplicationContext.CreateInstanceDI(); + obj.Data = _service.GetValue(); + obj.MarkAsOld(); + return obj; + } + } + + /// + /// Factory with parameterless, single-criteria and multi-criteria overloads + /// of the same operation, to exercise overload disambiguation. + /// + public class OverloadRootFactory : Csla.Server.ObjectFactory + { + public OverloadRootFactory(ApplicationContext applicationContext) + : base(applicationContext) { } + + public object Fetch() + { + var obj = ApplicationContext.CreateInstanceDI(); + obj.Data = "Fetch0"; + obj.MarkAsOld(); + return obj; + } + + public object Fetch(string text) + { + var obj = ApplicationContext.CreateInstanceDI(); + obj.Data = $"Fetch1 {text}"; + obj.MarkAsOld(); + return obj; + } + + public object Fetch(string text, int number) + { + var obj = ApplicationContext.CreateInstanceDI(); + obj.Data = $"Fetch2 {text} {number}"; + obj.MarkAsOld(); + return obj; + } + } +} diff --git a/Source/tests/Csla.test/ObjectFactory/ObjectFactoryTests.cs b/Source/tests/Csla.test/ObjectFactory/ObjectFactoryTests.cs index 438bf4f90b..5e56cc7e6e 100644 --- a/Source/tests/Csla.test/ObjectFactory/ObjectFactoryTests.cs +++ b/Source/tests/Csla.test/ObjectFactory/ObjectFactoryTests.cs @@ -10,6 +10,7 @@ using Csla.TestHelpers; using FluentAssertions; using FluentAssertions.Execution; +using Microsoft.Extensions.DependencyInjection; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Csla.Test.ObjectFactory @@ -330,5 +331,103 @@ public async Task BusinessObbjectFactory_ReturnTypesOfAsyncMethods() obj.Text.Should().NotBeNullOrWhiteSpace(); } } + + #region Multiple parameters and dependency injection (issue #1707) + + private static TestDIContext CreateFactoryContext(Action configureServices = null) + where TFactory : class + { + return TestDIContextFactory.CreateContext( + opts => opts.DataPortal(dp => dp.AddServerSideDataPortal( + cfg => cfg.RegisterObjectFactoryLoader>())), + new System.Security.Claims.ClaimsPrincipal(), + configureServices); + } + + [TestMethod("Factory Create accepts multiple criteria parameters")] + public void CreateWithMultipleParameters() + { + var testDIContext = CreateFactoryContext(); + var dataPortal = testDIContext.CreateDataPortal(); + + var root = dataPortal.Create("abc", 5); + + Assert.AreEqual("Create abc 5", root.Data); + Assert.IsTrue(root.IsNew); + } + + [TestMethod("Factory Fetch accepts multiple criteria parameters")] + public void FetchWithMultipleParameters() + { + var testDIContext = CreateFactoryContext(); + var dataPortal = testDIContext.CreateDataPortal(); + + var root = dataPortal.Fetch("abc", 5); + + Assert.AreEqual("Fetch abc 5", root.Data); + Assert.IsFalse(root.IsNew); + } + + [TestMethod("Factory method resolves [Inject] parameters from the service provider")] + public void FetchWithInjectedService() + { + var testDIContext = CreateFactoryContext( + services => services.AddTransient()); + var dataPortal = testDIContext.CreateDataPortal(); + + var root = dataPortal.Fetch("id7"); + + Assert.AreEqual("id7:injected", root.Data); + } + + [TestMethod("Factory method supports multiple criteria together with [Inject]")] + public void CreateWithMultipleParametersAndInjectedService() + { + var testDIContext = CreateFactoryContext( + services => services.AddTransient()); + var dataPortal = testDIContext.CreateDataPortal(); + + var root = dataPortal.Create("abc", 5); + + Assert.AreEqual("abc 5 injected", root.Data); + Assert.IsTrue(root.IsNew); + } + + [TestMethod("Factory overloads are disambiguated by criteria parameter count")] + public void FetchOverloadResolution() + { + var testDIContext = CreateFactoryContext(); + var dataPortal = testDIContext.CreateDataPortal(); + + Assert.AreEqual("Fetch0", dataPortal.Fetch().Data); + Assert.AreEqual("Fetch1 abc", dataPortal.Fetch("abc").Data); + Assert.AreEqual("Fetch2 abc 5", dataPortal.Fetch("abc", 5).Data); + } + + [TestMethod("Object factory supports constructor injection")] + public void FetchWithConstructorInjectedService() + { + var testDIContext = CreateFactoryContext( + services => services.AddTransient()); + var dataPortal = testDIContext.CreateDataPortal(); + + var root = dataPortal.Fetch(); + + Assert.AreEqual("injected", root.Data); + } + + [TestMethod("Command object factory Execute supports criteria plus [Inject]")] + public void ExecuteCommandWithInjectedService() + { + var testDIContext = TestDIContextFactory.CreateDefaultContext( + services => services.AddTransient()); + var dataPortal = testDIContext.CreateDataPortal(); + + var result = InjectCommandObject.Execute(dataPortal); + + Assert.AreEqual("injected", result.Value); + } + + #endregion } } \ No newline at end of file