diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index 757fdae5d3..0000000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(dotnet nbgv:*)", - "Bash(dotnet build:*)", - "Bash(dotnet run)", - "Bash(dotnet nuget list:*)", - "Bash(dotnet restore:*)", - "Bash(dotnet clean:*)", - "Bash(dotnet list:*)", - "Bash(findstr:*)", - "Bash(dir \"S:\\\\src\\\\rdl\\\\csla\\\\Samples\\\\RuleTutorial\\\\AuthzFactoryMethods\\\\*.cs\" /b)", - "Bash(powershell:*)", - "WebSearch" - ] - } -} diff --git a/.gitignore b/.gitignore index 82177b9ad3..44174d824d 100644 --- a/.gitignore +++ b/.gitignore @@ -183,3 +183,4 @@ Source/Csla.Xaml.Uwp/project.lock.json .vscode .idea Samples/ProjectTracker/ProjectTracker.Blazor/ProjectTracker.Blazor/PTracker.db +.claude/settings.local.json diff --git a/Source/Csla.Channels.Grpc/GrpcPortal.cs b/Source/Csla.Channels.Grpc/GrpcPortal.cs index 8475f9b746..5d662ec9b8 100644 --- a/Source/Csla.Channels.Grpc/GrpcPortal.cs +++ b/Source/Csla.Channels.Grpc/GrpcPortal.cs @@ -168,6 +168,7 @@ public async Task Create(CriteriaRequest request) request.ClientCulture, request.ClientUICulture, Deserialize(request.ClientContext)); + context.OperationName = request.OperationName; var dpr = await dataPortalServer.Create(objectType, criteria, context, true); @@ -216,6 +217,7 @@ public async Task Fetch(CriteriaRequest request) request.ClientCulture, request.ClientUICulture, Deserialize(request.ClientContext)); + context.OperationName = request.OperationName; var dpr = await dataPortalServer.Fetch(objectType, criteria, context, true); @@ -307,6 +309,7 @@ public async Task Delete(CriteriaRequest request) request.ClientCulture, request.ClientUICulture, Deserialize(request.ClientContext)); + context.OperationName = request.OperationName; var dpr = await dataPortalServer.Delete(objectType, criteria, context, true); diff --git a/Source/Csla.Channels.RabbitMq/RabbitMqPortal.cs b/Source/Csla.Channels.RabbitMq/RabbitMqPortal.cs index 2467171a83..1b2cae2d2a 100644 --- a/Source/Csla.Channels.RabbitMq/RabbitMqPortal.cs +++ b/Source/Csla.Channels.RabbitMq/RabbitMqPortal.cs @@ -158,11 +158,12 @@ private async Task Create(CriteriaRequest request) var objectType = Reflection.MethodCaller.GetType(AssemblyNameTranslator.GetAssemblyQualifiedName(request.TypeName)); var context = new DataPortalContext( - _applicationContext, Deserialize(request.Principal), + _applicationContext, Deserialize(request.Principal), true, request.ClientCulture, request.ClientUICulture, Deserialize(request.ClientContext)); + context.OperationName = request.OperationName; var dpr = await _dataPortalServer.Create(objectType, criteria, context, true); @@ -207,6 +208,7 @@ private async Task Fetch(CriteriaRequest request) request.ClientCulture, request.ClientUICulture, Deserialize(request.ClientContext)); + context.OperationName = request.OperationName; var dpr = await _dataPortalServer.Fetch(objectType, criteria, context, true); @@ -290,6 +292,7 @@ private async Task Delete(CriteriaRequest request) request.ClientCulture, request.ClientUICulture, Deserialize(request.ClientContext)); + context.OperationName = request.OperationName; var dpr = await _dataPortalServer.Delete(objectType, criteria, context, true); diff --git a/Source/Csla.Generators/cs/DataPortalInterfaces/Csla.Generator.DataPortalInterfaces.CSharp/DataPortalInterfaceBuilder.cs b/Source/Csla.Generators/cs/DataPortalInterfaces/Csla.Generator.DataPortalInterfaces.CSharp/DataPortalInterfaceBuilder.cs index daa1f10460..0e136807f6 100644 --- a/Source/Csla.Generators/cs/DataPortalInterfaces/Csla.Generator.DataPortalInterfaces.CSharp/DataPortalInterfaceBuilder.cs +++ b/Source/Csla.Generators/cs/DataPortalInterfaces/Csla.Generator.DataPortalInterfaces.CSharp/DataPortalInterfaceBuilder.cs @@ -39,6 +39,8 @@ internal GenerationResults BuildPartialTypeDefinition(ExtractedTypeDefinition ty AppendBlockStart(textWriter); AppendInvokeOperationAsyncMethod(textWriter, typeDefinition); + textWriter.WriteLine(); + AppendInvokeNamedOperationAsyncMethod(textWriter, typeDefinition); AppendBlockEnd(textWriter); AppendContainerDefinitionClosures(textWriter, typeDefinition); @@ -85,7 +87,7 @@ private void AppendTypeDefinition(IndentedTextWriter textWriter, ExtractedTypeDe textWriter.Write(" partial class "); textWriter.Write(typeDefinition.TypeName); textWriter.Write(typeDefinition.TypeParameters); - textWriter.WriteLine(" : Csla.Server.IDataPortalOperationMapping"); + textWriter.WriteLine(" : Csla.Server.IDataPortalOperationMapping, Csla.Server.IDataPortalOperationNamedMapping"); } private void AppendInvokeOperationAsyncMethod(IndentedTextWriter textWriter, ExtractedTypeDefinition typeDefinition) @@ -233,6 +235,42 @@ private static Dictionary> GroupMethodsBy return result; } + private void AppendInvokeNamedOperationAsyncMethod(IndentedTextWriter textWriter, ExtractedTypeDefinition typeDefinition) + { + textWriter.WriteLine("async global::System.Threading.Tasks.Task Csla.Server.IDataPortalOperationNamedMapping.InvokeNamedOperationAsync("); + textWriter.Indent++; + textWriter.WriteLine("string operationName, bool isSync, object?[]? criteria, global::System.IServiceProvider serviceProvider)"); + textWriter.Indent--; + AppendBlockStart(textWriter); + + textWriter.WriteLine("switch (operationName)"); + AppendBlockStart(textWriter); + + // Collect all unique operation names across all methods + var emittedNames = new HashSet(); + foreach (var method in typeDefinition.OperationMethods) + { + foreach (var attr in method.OperationAttributeNames) + { + var operationName = method.GetOperationName(attr); + if (emittedNames.Add(operationName)) + { + textWriter.WriteLine($"case \"{operationName}\":"); + textWriter.Indent++; + AppendMethodDispatch(textWriter, method); + textWriter.WriteLine("break;"); + textWriter.Indent--; + } + } + } + + AppendBlockEnd(textWriter); + + textWriter.WriteLine("throw new Csla.Server.DataPortalOperationNotSupportedException(operationName, criteria);"); + + AppendBlockEnd(textWriter); + } + private static string SanitizeTypeName(string typeName) { // Remove global:: prefix and dots, convert to safe identifier diff --git a/Source/Csla.Generators/cs/DataPortalInterfaces/Csla.Generator.DataPortalInterfaces.CSharp/Discovery/OperationMethodExtractor.cs b/Source/Csla.Generators/cs/DataPortalInterfaces/Csla.Generator.DataPortalInterfaces.CSharp/Discovery/OperationMethodExtractor.cs index a6dd215c34..3f7e1de899 100644 --- a/Source/Csla.Generators/cs/DataPortalInterfaces/Csla.Generator.DataPortalInterfaces.CSharp/Discovery/OperationMethodExtractor.cs +++ b/Source/Csla.Generators/cs/DataPortalInterfaces/Csla.Generator.DataPortalInterfaces.CSharp/Discovery/OperationMethodExtractor.cs @@ -154,9 +154,49 @@ private static ExtractedOperationParameter ExtractParameter(IParameterSymbol par Name = param.Name, TypeFullName = param.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), TypeDisplayName = param.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + TypeMetadataName = GetOperationTypeKey(param.Type), IsInjected = isInjected, AllowNull = allowNull }; } + + /// + /// Computes a deterministic type key for use in operation names. + /// Arrays: elementType + "Array" (e.g. "Int32Array") + /// Generic types: MetadataName with backtick replaced, then type args (e.g. "List_1_Int32") + /// Simple types: MetadataName (e.g. "Int32", "String") + /// + internal static string GetOperationTypeKey(ITypeSymbol typeSymbol) + { + if (typeSymbol is IArrayTypeSymbol arrayType) + { + return GetOperationTypeKey(arrayType.ElementType) + "Array"; + } + + if (typeSymbol is INamedTypeSymbol namedType && namedType.IsGenericType) + { + var baseName = namedType.MetadataName.Replace('`', '_'); + var typeArgs = string.Join("_", namedType.TypeArguments.Select(GetOperationTypeKey)); + return baseName + "_" + typeArgs; + } + + return typeSymbol.MetadataName; + } + + /// + /// Gets the base operation name from an attribute fully-qualified name. + /// Strips namespace and "Attribute" suffix + /// (e.g. "Csla.FetchAttribute" -> "Fetch") + /// + internal static string GetBaseOperationName(string attributeFullName) + { + var name = attributeFullName; + var dotIndex = name.LastIndexOf('.'); + if (dotIndex >= 0) + name = name.Substring(dotIndex + 1); + if (name.EndsWith("Attribute")) + name = name.Substring(0, name.Length - "Attribute".Length); + return name; + } } } diff --git a/Source/Csla.Generators/cs/DataPortalInterfaces/Csla.Generator.DataPortalInterfaces.CSharp/Extractors/ExtractedOperationMethod.cs b/Source/Csla.Generators/cs/DataPortalInterfaces/Csla.Generator.DataPortalInterfaces.CSharp/Extractors/ExtractedOperationMethod.cs index b07d894e8c..378f379d89 100644 --- a/Source/Csla.Generators/cs/DataPortalInterfaces/Csla.Generator.DataPortalInterfaces.CSharp/Extractors/ExtractedOperationMethod.cs +++ b/Source/Csla.Generators/cs/DataPortalInterfaces/Csla.Generator.DataPortalInterfaces.CSharp/Extractors/ExtractedOperationMethod.cs @@ -7,6 +7,8 @@ //----------------------------------------------------------------------- using System.Collections.Generic; +using System.Linq; +using Csla.Generator.DataPortalInterfaces.CSharp.Discovery; namespace Csla.Generator.DataPortalInterfaces.CSharp.Extractors { @@ -43,5 +45,18 @@ public class ExtractedOperationMethod /// The injected parameters (marked with [Inject]) /// public IList InjectParameters { get; } = new List(); + + /// + /// Computes the deterministic operation name for a given attribute. + /// Format: "OperationType" for no criteria, "OperationType__Type1_Type2" for criteria. + /// + public string GetOperationName(string attributeFullName) + { + var baseName = OperationMethodExtractor.GetBaseOperationName(attributeFullName); + if (CriteriaParameters.Count == 0) + return baseName; + var typeKeys = string.Join("_", CriteriaParameters.Select(p => p.TypeMetadataName)); + return baseName + "__" + typeKeys; + } } } diff --git a/Source/Csla.Generators/cs/DataPortalInterfaces/Csla.Generator.DataPortalInterfaces.CSharp/Extractors/ExtractedOperationParameter.cs b/Source/Csla.Generators/cs/DataPortalInterfaces/Csla.Generator.DataPortalInterfaces.CSharp/Extractors/ExtractedOperationParameter.cs index f3d59fe0bb..dc7d57fcd3 100644 --- a/Source/Csla.Generators/cs/DataPortalInterfaces/Csla.Generator.DataPortalInterfaces.CSharp/Extractors/ExtractedOperationParameter.cs +++ b/Source/Csla.Generators/cs/DataPortalInterfaces/Csla.Generator.DataPortalInterfaces.CSharp/Extractors/ExtractedOperationParameter.cs @@ -31,6 +31,12 @@ public class ExtractedOperationParameter /// public string TypeDisplayName { get; set; } = string.Empty; + /// + /// The type's metadata name used for operation name computation + /// (e.g. "Int32", "String", "List_1_Int32") + /// + public string TypeMetadataName { get; set; } = string.Empty; + /// /// Whether this is an injected parameter /// diff --git a/Source/Csla.Web.Mvc.Shared/Server/Hosts/HttpPortal.cs b/Source/Csla.Web.Mvc.Shared/Server/Hosts/HttpPortal.cs index f90b931f8e..95099443f7 100644 --- a/Source/Csla.Web.Mvc.Shared/Server/Hosts/HttpPortal.cs +++ b/Source/Csla.Web.Mvc.Shared/Server/Hosts/HttpPortal.cs @@ -64,6 +64,7 @@ public async Task Create(CriteriaRequest request) request.ClientCulture, request.ClientUICulture, DeserializeRequired(request.ClientContext)); + context.OperationName = request.OperationName; var dpr = await dataPortalServer.Create(objectType, criteria, context, true); @@ -112,6 +113,7 @@ public async Task Fetch(CriteriaRequest request) request.ClientCulture, request.ClientUICulture, DeserializeRequired(request.ClientContext)); + context.OperationName = request.OperationName; var dpr = await dataPortalServer.Fetch(objectType, criteria, context, true); @@ -197,12 +199,13 @@ public async Task Delete(CriteriaRequest request) var objectType = Reflection.MethodCaller.GetType(AssemblyNameTranslator.GetAssemblyQualifiedName(request.TypeName)); var context = new DataPortalContext( - _applicationContext, + _applicationContext, Deserialize(request.Principal), true, request.ClientCulture, request.ClientUICulture, DeserializeRequired(request.ClientContext)); + context.OperationName = request.OperationName; var dpr = await dataPortalServer.Delete(objectType, criteria, context, true); diff --git a/Source/Csla/DataPortalClient/DataPortalProxy.cs b/Source/Csla/DataPortalClient/DataPortalProxy.cs index 0899423272..a13c3e3711 100644 --- a/Source/Csla/DataPortalClient/DataPortalProxy.cs +++ b/Source/Csla/DataPortalClient/DataPortalProxy.cs @@ -65,7 +65,8 @@ public async virtual Task Create([DynamicallyAccessedMembers(D { var request = GetBaseCriteriaRequest(criteria); request.TypeName = AssemblyNameTranslator.GetAssemblyQualifiedName(objectType.AssemblyQualifiedName!); - + request.OperationName = context.OperationName; + request = ConvertRequest(request); var serialized = ApplicationContext.GetRequiredService().Serialize(request); serialized = await CallDataPortalServer(serialized, "create", GetRoutingToken(objectType), isSync).ConfigureAwait(false); @@ -114,6 +115,7 @@ public async virtual Task Fetch([DynamicallyAccessedMembers(Dy { var request = GetBaseCriteriaRequest(criteria); request.TypeName = AssemblyNameTranslator.GetAssemblyQualifiedName(objectType.AssemblyQualifiedName!); + request.OperationName = context.OperationName; request = ConvertRequest(request); var serialized = ApplicationContext.GetRequiredService().Serialize(request); @@ -212,6 +214,7 @@ public async virtual Task Delete([DynamicallyAccessedMembers(D { var request = GetBaseCriteriaRequest(criteria); request.TypeName = AssemblyNameTranslator.GetAssemblyQualifiedName(objectType.AssemblyQualifiedName!); + request.OperationName = context.OperationName; request = ConvertRequest(request); var serialized = ApplicationContext.GetRequiredService().Serialize(request); diff --git a/Source/Csla/DataPortalT.cs b/Source/Csla/DataPortalT.cs index 26b52b01a6..5e99400e3d 100644 --- a/Source/Csla/DataPortalT.cs +++ b/Source/Csla/DataPortalT.cs @@ -113,6 +113,8 @@ private async Task DoCreateAsync([DynamicallyAccessedMembers(Dynamically var proxy = GetDataPortalProxy(method); var dpContext = new Server.DataPortalContext(_applicationContext, proxy.IsServerRemote); + if (method != null) + dpContext.OperationName = Server.DataPortalOperationNameHelper.ComputeOperationName(method.MethodInfo); Server.DataPortalResult result = default!; try @@ -190,6 +192,8 @@ private async Task DoFetchAsync([DynamicallyAccessedMembers(DynamicallyA var proxy = GetDataPortalProxy(method); var dpContext = new Server.DataPortalContext(_applicationContext, proxy.IsServerRemote); + if (method != null) + dpContext.OperationName = Server.DataPortalOperationNameHelper.ComputeOperationName(method.MethodInfo); Server.DataPortalResult result = default!; try @@ -225,6 +229,8 @@ private async Task DoExecuteAsync([DynamicallyAccessedMembers(Dynamicall _ = ServiceProviderMethodCaller.TryGetProviderMethodInfoFor(objectType, criteria, out var method); var proxy = GetDataPortalProxy(method); var dpContext = new Server.DataPortalContext(_applicationContext, proxy.IsServerRemote); + if (method != null) + dpContext.OperationName = Server.DataPortalOperationNameHelper.ComputeOperationName(method.MethodInfo); Server.DataPortalResult result = default!; try @@ -510,6 +516,8 @@ private async Task DoDeleteAsync([DynamicallyAccessedMembers(DynamicallyAccessed var proxy = GetDataPortalProxy(method); var dpContext = new Server.DataPortalContext(_applicationContext, proxy.IsServerRemote); + if (method != null) + dpContext.OperationName = Server.DataPortalOperationNameHelper.ComputeOperationName(method.MethodInfo); try { var result = await _cache.GetDataPortalResultAsync(objectType, criteria, DataPortalOperations.Delete, diff --git a/Source/Csla/Server/DataPortalContext.cs b/Source/Csla/Server/DataPortalContext.cs index c8cab4d509..6ca9a14af6 100644 --- a/Source/Csla/Server/DataPortalContext.cs +++ b/Source/Csla/Server/DataPortalContext.cs @@ -51,6 +51,12 @@ public class DataPortalContext : Serialization.Mobile.IMobileObject, IUseApplica /// public string ClientUICulture { get; private set; } + /// + /// Gets or sets the operation name for name-based dispatch. + /// Null when sent from legacy clients. + /// + public string? OperationName { get; set; } + internal IContextDictionary? ClientContext { get; private set; } /// @@ -148,6 +154,7 @@ void Serialization.Mobile.IMobileObject.GetState(Serialization.Mobile.Serializat info.AddValue("clientCulture", ClientCulture); info.AddValue("clientUICulture", ClientUICulture); info.AddValue("isRemotePortal", IsRemotePortal); + info.AddValue("operationName", OperationName); } void Serialization.Mobile.IMobileObject.GetChildren(Serialization.Mobile.SerializationInfo info, Serialization.Mobile.MobileFormatter formatter) @@ -161,6 +168,7 @@ void Serialization.Mobile.IMobileObject.SetState(Serialization.Mobile.Serializat ClientCulture = info.GetValue("clientCulture")!; ClientUICulture = info.GetValue("clientUICulture")!; IsRemotePortal = info.GetValue("isRemotePortal"); + OperationName = info.GetValue("operationName"); } void Serialization.Mobile.IMobileObject.SetChildren(Serialization.Mobile.SerializationInfo info, Serialization.Mobile.MobileFormatter formatter) diff --git a/Source/Csla/Server/DataPortalOperationNameHelper.cs b/Source/Csla/Server/DataPortalOperationNameHelper.cs new file mode 100644 index 0000000000..44762ed609 --- /dev/null +++ b/Source/Csla/Server/DataPortalOperationNameHelper.cs @@ -0,0 +1,69 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Marimer LLC. All rights reserved. +// Website: https://cslanet.com +// +// Computes deterministic operation names for data portal dispatch +//----------------------------------------------------------------------- + +namespace Csla.Server +{ + /// + /// Computes deterministic operation names for name-based data portal dispatch. + /// The algorithm must agree with the source generator's naming logic. + /// + internal static class DataPortalOperationNameHelper + { + /// + /// Computes the operation name from a resolved method and its operation attribute type. + /// + internal static string ComputeOperationName(System.Reflection.MethodInfo methodInfo) + where T : DataPortalOperationAttribute + { + var baseName = GetOperationBaseName(); + var parameters = methodInfo.GetParameters(); + var criteriaTypeKeys = parameters + .Where(p => !p.GetCustomAttributes(typeof(InjectAttribute), false).Any()) + .Select(p => GetTypeKey(p.ParameterType)) + .ToArray(); + + if (criteriaTypeKeys.Length == 0) + return baseName; + + return baseName + "__" + string.Join("_", criteriaTypeKeys); + } + + /// + /// Gets the base operation name from an attribute type. + /// Strips the "Attribute" suffix (e.g. typeof(FetchAttribute) -> "Fetch"). + /// + private static string GetOperationBaseName() + where T : DataPortalOperationAttribute + { + var name = typeof(T).Name; + if (name.EndsWith("Attribute")) + name = name[..^"Attribute".Length]; + return name; + } + + /// + /// Computes a deterministic type key from a runtime Type. + /// Must produce the same result as the generator's GetOperationTypeKey(ITypeSymbol). + /// + internal static string GetTypeKey(Type type) + { + if (type.IsArray) + return GetTypeKey(type.GetElementType()!) + "Array"; + + if (type.IsGenericType) + { + // Type.Name for generics looks like "List`1" - same as MetadataName + var baseName = type.Name.Replace('`', '_'); + var typeArgs = string.Join("_", type.GetGenericArguments().Select(GetTypeKey)); + return baseName + "_" + typeArgs; + } + + return type.Name; + } + } +} diff --git a/Source/Csla/Server/DataPortalOperationNotSupportedException.cs b/Source/Csla/Server/DataPortalOperationNotSupportedException.cs index 5a221e0ee1..4099d31e71 100644 --- a/Source/Csla/Server/DataPortalOperationNotSupportedException.cs +++ b/Source/Csla/Server/DataPortalOperationNotSupportedException.cs @@ -27,11 +27,28 @@ public DataPortalOperationNotSupportedException(Type operationType, object?[]? c Criteria = criteria; } + /// + /// Creates a new instance of the exception for name-based dispatch. + /// + /// The operation name that was not matched + /// The criteria that was not matched + public DataPortalOperationNotSupportedException(string operationName, object?[]? criteria) + : base($"No generated dispatch found for operation '{operationName}' with {criteria?.Length ?? 0} criteria parameters.") + { + OperationName = operationName; + Criteria = criteria; + } + /// /// Gets the operation attribute type that was not matched. /// public Type? OperationType { get; } + /// + /// Gets the operation name that was not matched. + /// + public string? OperationName { get; } + /// /// Gets the criteria that was not matched. /// diff --git a/Source/Csla/Server/DataPortalTarget.cs b/Source/Csla/Server/DataPortalTarget.cs index c65453d54f..83147b2fe2 100644 --- a/Source/Csla/Server/DataPortalTarget.cs +++ b/Source/Csla/Server/DataPortalTarget.cs @@ -28,6 +28,7 @@ internal class DataPortalTarget : LateBoundObject private readonly IDataPortalTarget? _target; private readonly IDataPortalOperationMapping? _operationMapping; + private readonly IDataPortalOperationNamedMapping? _namedMapping; private readonly ApplicationContext _applicationContext; private readonly TimeSpan _waitForIdleTimeout; private readonly DataPortalMethodNames _methodNames; @@ -37,6 +38,7 @@ public DataPortalTarget(object obj, ApplicationContext applicationContext, Confi { _target = obj as IDataPortalTarget; _operationMapping = obj as IDataPortalOperationMapping; + _namedMapping = obj as IDataPortalOperationNamedMapping; _applicationContext = applicationContext; _waitForIdleTimeout = TimeSpan.FromSeconds(cslaOptions.DefaultWaitForIdleTimeoutInSeconds); @@ -155,16 +157,36 @@ internal void MarkOld() CallMethodIfImplemented("MarkOld"); } - private async Task InvokeOperationAsync(object criteria, bool isSync) + private async Task InvokeOperationAsync(object criteria, bool isSync, string? operationName = null) where T : DataPortalOperationAttribute { + var criteriaArray = DataPortal.GetCriteriaArray(criteria); + + // Try name-based dispatch first + if (operationName != null && _namedMapping != null) + { + try + { + var serviceProvider = _applicationContext.CurrentServiceProvider; + await _namedMapping.InvokeNamedOperationAsync( + operationName, isSync, criteriaArray, serviceProvider + ).ConfigureAwait(false); + return; + } + catch (DataPortalOperationNotSupportedException) + { + // Fall through to criteria-based path + } + } + + // Try criteria-based dispatch if (_operationMapping != null) { try { var serviceProvider = _applicationContext.CurrentServiceProvider; await _operationMapping.InvokeOperationAsync( - typeof(T), isSync, DataPortal.GetCriteriaArray(criteria), serviceProvider + typeof(T), isSync, criteriaArray, serviceProvider ).ConfigureAwait(false); return; } @@ -173,7 +195,8 @@ await _operationMapping.InvokeOperationAsync( // Fall through to reflection path } } - object?[] parameters = DataPortal.GetCriteriaArray(criteria)!; + + object?[] parameters = criteriaArray!; await CallMethodTryAsyncDI(isSync, parameters).ConfigureAwait(false); } @@ -198,9 +221,9 @@ await _operationMapping.InvokeOperationAsync( await CallMethodTryAsyncDI(false, parameters).ConfigureAwait(false); } - public Task CreateAsync(object criteria, bool isSync) + public Task CreateAsync(object criteria, bool isSync, string? operationName = null) { - return InvokeOperationAsync(criteria, isSync); + return InvokeOperationAsync(criteria, isSync, operationName); } public Task CreateChildAsync(params object?[]? parameters) @@ -208,9 +231,9 @@ public Task CreateChildAsync(params object?[]? parameters) return InvokeChildOperationAsync(parameters); } - public Task FetchAsync(object criteria, bool isSync) + public Task FetchAsync(object criteria, bool isSync, string? operationName = null) { - return InvokeOperationAsync(criteria, isSync); + return InvokeOperationAsync(criteria, isSync, operationName); } public Task FetchChildAsync(params object?[]? parameters) @@ -218,9 +241,9 @@ public Task FetchChildAsync(params object?[]? parameters) return InvokeChildOperationAsync(parameters); } - public Task ExecuteAsync(object criteria, bool isSync) + public Task ExecuteAsync(object criteria, bool isSync, string? operationName = null) { - return InvokeOperationAsync(criteria, isSync); + return InvokeOperationAsync(criteria, isSync, operationName); } public async Task UpdateAsync(bool isSync) @@ -232,7 +255,7 @@ public async Task UpdateAsync(bool isSync) if (!busObj.IsNew) { // tell the object to delete itself - await InvokeOperationAsync(EmptyCriteria.Instance, isSync).ConfigureAwait(false); + await InvokeOperationAsync(EmptyCriteria.Instance, isSync, "DeleteSelf").ConfigureAwait(false); } MarkNew(); } @@ -241,12 +264,12 @@ public async Task UpdateAsync(bool isSync) if (busObj.IsNew) { // tell the object to insert itself - await InvokeOperationAsync(EmptyCriteria.Instance, isSync).ConfigureAwait(false); + await InvokeOperationAsync(EmptyCriteria.Instance, isSync, "Insert").ConfigureAwait(false); } else { // tell the object to update itself - await InvokeOperationAsync(EmptyCriteria.Instance, isSync).ConfigureAwait(false); + await InvokeOperationAsync(EmptyCriteria.Instance, isSync, "Update").ConfigureAwait(false); } MarkOld(); } @@ -256,7 +279,7 @@ public async Task UpdateAsync(bool isSync) // this is an updatable collection or some other // non-BusinessBase type of object // tell the object to update itself - await InvokeOperationAsync(EmptyCriteria.Instance, isSync).ConfigureAwait(false); + await InvokeOperationAsync(EmptyCriteria.Instance, isSync, "Update").ConfigureAwait(false); MarkOld(); } } @@ -308,12 +331,12 @@ public async Task UpdateChildAsync(params object?[]? parameters) public Task ExecuteAsync(bool isSync) { - return InvokeOperationAsync(EmptyCriteria.Instance, isSync); + return InvokeOperationAsync(EmptyCriteria.Instance, isSync, "Execute"); } - public Task DeleteAsync(object criteria, bool isSync) + public Task DeleteAsync(object criteria, bool isSync, string? operationName = null) { - return InvokeOperationAsync(criteria, isSync); + return InvokeOperationAsync(criteria, isSync, operationName); } #if NET8_0_OR_GREATER diff --git a/Source/Csla/Server/Hosts/DataPortalChannel/CriteriaRequest.cs b/Source/Csla/Server/Hosts/DataPortalChannel/CriteriaRequest.cs index 14a10d8b31..56eaaaaa9e 100644 --- a/Source/Csla/Server/Hosts/DataPortalChannel/CriteriaRequest.cs +++ b/Source/Csla/Server/Hosts/DataPortalChannel/CriteriaRequest.cs @@ -111,6 +111,22 @@ public string ClientUICulture set => LoadProperty(ClientUICultureProperty, value ?? throw new ArgumentNullException(nameof(ClientUICulture))); } + /// + /// The operation name for name-based dispatch. + /// Null for legacy clients. + /// + public static readonly PropertyInfo OperationNameProperty = RegisterProperty(nameof(OperationName)); + + /// + /// The operation name for name-based dispatch. + /// Null for legacy clients. + /// + public string? OperationName + { + get => GetProperty(OperationNameProperty); + set => LoadProperty(OperationNameProperty, value); + } + /// /// Initializes a new instance of -object. /// diff --git a/Source/Csla/Server/IDataPortalOperationNamedMapping.cs b/Source/Csla/Server/IDataPortalOperationNamedMapping.cs new file mode 100644 index 0000000000..9677c97084 --- /dev/null +++ b/Source/Csla/Server/IDataPortalOperationNamedMapping.cs @@ -0,0 +1,30 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Marimer LLC. All rights reserved. +// Website: https://cslanet.com +// +// Interface for name-based data portal operation dispatch +//----------------------------------------------------------------------- + +namespace Csla.Server +{ + /// + /// Implemented by source-generated code to provide name-based + /// data portal operation method invocation without reflection. + /// + public interface IDataPortalOperationNamedMapping + { + /// + /// Invoke a data portal operation method by name. + /// + /// The deterministic operation name (e.g. "Fetch__Int32") + /// Whether the client is calling synchronously + /// The criteria parameters, or null/empty for no-criteria operations + /// Service provider for resolving injected dependencies + /// A Task representing the async operation + /// + /// Thrown when the operation name is not handled by the generated code + /// + Task InvokeNamedOperationAsync(string operationName, bool isSync, object?[]? criteria, IServiceProvider serviceProvider); + } +} diff --git a/Source/Csla/Server/SimpleDataPortal.cs b/Source/Csla/Server/SimpleDataPortal.cs index 0a5c8b269d..faa6051470 100644 --- a/Source/Csla/Server/SimpleDataPortal.cs +++ b/Source/Csla/Server/SimpleDataPortal.cs @@ -58,7 +58,7 @@ public async Task Create([DynamicallyAccessedMembers(Dynamical obj = _applicationContext.CreateInstanceDI(_applicationContext.CreateInstanceDI(objectType)); obj.OnDataPortalInvoke(eventArgs); obj.MarkNew(); - await obj.CreateAsync(criteria, isSync).ConfigureAwait(false); + await obj.CreateAsync(criteria, isSync, context.OperationName).ConfigureAwait(false); await obj.WaitForIdle().ConfigureAwait(false); obj.ThrowIfBusy(); obj.OnDataPortalInvokeComplete(eventArgs); @@ -115,7 +115,7 @@ public async Task Fetch([DynamicallyAccessedMembers(Dynamicall _activator.InitializeInstance(obj.Instance); obj.OnDataPortalInvoke(eventArgs); obj.MarkOld(); - await obj.FetchAsync(criteria, isSync).ConfigureAwait(false); + await obj.FetchAsync(criteria, isSync, context.OperationName).ConfigureAwait(false); await obj.WaitForIdle().ConfigureAwait(false); obj.ThrowIfBusy(); obj.OnDataPortalInvokeComplete(eventArgs); @@ -156,7 +156,7 @@ private async Task Execute([DynamicallyAccessedMembers(Dynamic _activator.InitializeInstance(obj.Instance); obj.OnDataPortalInvoke(eventArgs); obj.MarkOld(); - await obj.ExecuteAsync(criteria, isSync).ConfigureAwait(false); + await obj.ExecuteAsync(criteria, isSync, context.OperationName).ConfigureAwait(false); await obj.WaitForIdle().ConfigureAwait(false); obj.ThrowIfBusy(); obj.OnDataPortalInvokeComplete(eventArgs); @@ -282,7 +282,7 @@ public async Task Delete([DynamicallyAccessedMembers(Dynamical obj = _applicationContext.CreateInstanceDI(_applicationContext.CreateInstanceDI(objectType)); _activator.InitializeInstance(obj.Instance); obj.OnDataPortalInvoke(eventArgs); - await obj.DeleteAsync(criteria, isSync).ConfigureAwait(false); + await obj.DeleteAsync(criteria, isSync, context.OperationName).ConfigureAwait(false); await obj.WaitForIdle().ConfigureAwait(false); obj.ThrowIfBusy(); obj.OnDataPortalInvokeComplete(eventArgs); diff --git a/Source/tests/Csla.Generator.DataPortalInterfaces.CSharp.Tests/Helpers/Snapshots/DataPortalInterfaceGeneratorTests.AsyncMethod#TestApp.PersonEdit.DataPortalOperations.g.verified.cs b/Source/tests/Csla.Generator.DataPortalInterfaces.CSharp.Tests/Helpers/Snapshots/DataPortalInterfaceGeneratorTests.AsyncMethod#TestApp.PersonEdit.DataPortalOperations.g.verified.cs index 20f96a6239..825b39f51c 100644 --- a/Source/tests/Csla.Generator.DataPortalInterfaces.CSharp.Tests/Helpers/Snapshots/DataPortalInterfaceGeneratorTests.AsyncMethod#TestApp.PersonEdit.DataPortalOperations.g.verified.cs +++ b/Source/tests/Csla.Generator.DataPortalInterfaces.CSharp.Tests/Helpers/Snapshots/DataPortalInterfaceGeneratorTests.AsyncMethod#TestApp.PersonEdit.DataPortalOperations.g.verified.cs @@ -4,7 +4,7 @@ namespace TestApp { - public partial class PersonEdit : Csla.Server.IDataPortalOperationMapping + public partial class PersonEdit : Csla.Server.IDataPortalOperationMapping, Csla.Server.IDataPortalOperationNamedMapping { async global::System.Threading.Tasks.Task Csla.Server.IDataPortalOperationMapping.InvokeOperationAsync( global::System.Type operationType, bool isSync, object?[]? criteria, global::System.IServiceProvider serviceProvider) @@ -19,5 +19,21 @@ public partial class PersonEdit : Csla.Server.IDataPortalOperationMapping } throw new Csla.Server.DataPortalOperationNotSupportedException(operationType, criteria); } + + async global::System.Threading.Tasks.Task Csla.Server.IDataPortalOperationNamedMapping.InvokeNamedOperationAsync( + string operationName, bool isSync, object?[]? criteria, global::System.IServiceProvider serviceProvider) + { + switch (operationName) + { + case "Fetch__Int32": + if (criteria is { Length: 1 } && criteria[0] is int p0_int) + { + await Fetch(p0_int).ConfigureAwait(false); + return; + } + break; + } + throw new Csla.Server.DataPortalOperationNotSupportedException(operationName, criteria); + } } } diff --git a/Source/tests/Csla.Generator.DataPortalInterfaces.CSharp.Tests/Helpers/Snapshots/DataPortalInterfaceGeneratorTests.ChildOperations#TestApp.LineItem.DataPortalOperations.g.verified.cs b/Source/tests/Csla.Generator.DataPortalInterfaces.CSharp.Tests/Helpers/Snapshots/DataPortalInterfaceGeneratorTests.ChildOperations#TestApp.LineItem.DataPortalOperations.g.verified.cs index 4576e85bc2..6dcbb4cab2 100644 --- a/Source/tests/Csla.Generator.DataPortalInterfaces.CSharp.Tests/Helpers/Snapshots/DataPortalInterfaceGeneratorTests.ChildOperations#TestApp.LineItem.DataPortalOperations.g.verified.cs +++ b/Source/tests/Csla.Generator.DataPortalInterfaces.CSharp.Tests/Helpers/Snapshots/DataPortalInterfaceGeneratorTests.ChildOperations#TestApp.LineItem.DataPortalOperations.g.verified.cs @@ -4,7 +4,7 @@ namespace TestApp { - public partial class LineItem : Csla.Server.IDataPortalOperationMapping + public partial class LineItem : Csla.Server.IDataPortalOperationMapping, Csla.Server.IDataPortalOperationNamedMapping { async global::System.Threading.Tasks.Task Csla.Server.IDataPortalOperationMapping.InvokeOperationAsync( global::System.Type operationType, bool isSync, object?[]? criteria, global::System.IServiceProvider serviceProvider) @@ -54,5 +54,52 @@ public partial class LineItem : Csla.Server.IDataPortalOperationMapping } throw new Csla.Server.DataPortalOperationNotSupportedException(operationType, criteria); } + + async global::System.Threading.Tasks.Task Csla.Server.IDataPortalOperationNamedMapping.InvokeNamedOperationAsync( + string operationName, bool isSync, object?[]? criteria, global::System.IServiceProvider serviceProvider) + { + switch (operationName) + { + case "CreateChild": + if (criteria is null or { Length: 0 }) + { + CreateChild(); + return; + } + break; + case "FetchChild__Int32": + if (criteria is { Length: 1 } && criteria[0] is int p0_int) + { + FetchChild(p0_int); + return; + } + break; + case "InsertChild": + if (criteria is null or { Length: 0 }) + { + var dal = (global::TestApp.IDal)(serviceProvider.GetService(typeof(global::TestApp.IDal)) ?? throw new global::System.InvalidOperationException($"No service for type '{typeof(global::TestApp.IDal)}' has been registered.")); + await InsertChild(dal).ConfigureAwait(false); + return; + } + break; + case "UpdateChild": + if (criteria is null or { Length: 0 }) + { + var dal = (global::TestApp.IDal)(serviceProvider.GetService(typeof(global::TestApp.IDal)) ?? throw new global::System.InvalidOperationException($"No service for type '{typeof(global::TestApp.IDal)}' has been registered.")); + await UpdateChild(dal).ConfigureAwait(false); + return; + } + break; + case "DeleteSelfChild": + if (criteria is null or { Length: 0 }) + { + var dal = (global::TestApp.IDal)(serviceProvider.GetService(typeof(global::TestApp.IDal)) ?? throw new global::System.InvalidOperationException($"No service for type '{typeof(global::TestApp.IDal)}' has been registered.")); + await DeleteSelfChild(dal).ConfigureAwait(false); + return; + } + break; + } + throw new Csla.Server.DataPortalOperationNotSupportedException(operationName, criteria); + } } } diff --git a/Source/tests/Csla.Generator.DataPortalInterfaces.CSharp.Tests/Helpers/Snapshots/DataPortalInterfaceGeneratorTests.CompleteCrud#TestApp.PersonEdit.DataPortalOperations.g.verified.cs b/Source/tests/Csla.Generator.DataPortalInterfaces.CSharp.Tests/Helpers/Snapshots/DataPortalInterfaceGeneratorTests.CompleteCrud#TestApp.PersonEdit.DataPortalOperations.g.verified.cs index 15127243e7..ab81066256 100644 --- a/Source/tests/Csla.Generator.DataPortalInterfaces.CSharp.Tests/Helpers/Snapshots/DataPortalInterfaceGeneratorTests.CompleteCrud#TestApp.PersonEdit.DataPortalOperations.g.verified.cs +++ b/Source/tests/Csla.Generator.DataPortalInterfaces.CSharp.Tests/Helpers/Snapshots/DataPortalInterfaceGeneratorTests.CompleteCrud#TestApp.PersonEdit.DataPortalOperations.g.verified.cs @@ -4,7 +4,7 @@ namespace TestApp { - public partial class PersonEdit : Csla.Server.IDataPortalOperationMapping + public partial class PersonEdit : Csla.Server.IDataPortalOperationMapping, Csla.Server.IDataPortalOperationNamedMapping { async global::System.Threading.Tasks.Task Csla.Server.IDataPortalOperationMapping.InvokeOperationAsync( global::System.Type operationType, bool isSync, object?[]? criteria, global::System.IServiceProvider serviceProvider) @@ -54,5 +54,52 @@ public partial class PersonEdit : Csla.Server.IDataPortalOperationMapping } throw new Csla.Server.DataPortalOperationNotSupportedException(operationType, criteria); } + + async global::System.Threading.Tasks.Task Csla.Server.IDataPortalOperationNamedMapping.InvokeNamedOperationAsync( + string operationName, bool isSync, object?[]? criteria, global::System.IServiceProvider serviceProvider) + { + switch (operationName) + { + case "Create": + if (criteria is null or { Length: 0 }) + { + Create(); + return; + } + break; + case "Fetch__Int32": + if (criteria is { Length: 1 } && criteria[0] is int p0_int) + { + Fetch(p0_int); + return; + } + break; + case "Insert": + if (criteria is null or { Length: 0 }) + { + var dal = (global::TestApp.IDal)(serviceProvider.GetService(typeof(global::TestApp.IDal)) ?? throw new global::System.InvalidOperationException($"No service for type '{typeof(global::TestApp.IDal)}' has been registered.")); + await Insert(dal).ConfigureAwait(false); + return; + } + break; + case "Update": + if (criteria is null or { Length: 0 }) + { + var dal = (global::TestApp.IDal)(serviceProvider.GetService(typeof(global::TestApp.IDal)) ?? throw new global::System.InvalidOperationException($"No service for type '{typeof(global::TestApp.IDal)}' has been registered.")); + await Update(dal).ConfigureAwait(false); + return; + } + break; + case "DeleteSelf": + if (criteria is null or { Length: 0 }) + { + var dal = (global::TestApp.IDal)(serviceProvider.GetService(typeof(global::TestApp.IDal)) ?? throw new global::System.InvalidOperationException($"No service for type '{typeof(global::TestApp.IDal)}' has been registered.")); + await DeleteSelf(dal).ConfigureAwait(false); + return; + } + break; + } + throw new Csla.Server.DataPortalOperationNotSupportedException(operationName, criteria); + } } } diff --git a/Source/tests/Csla.Generator.DataPortalInterfaces.CSharp.Tests/Helpers/Snapshots/DataPortalInterfaceGeneratorTests.DeleteWithCriteria#TestApp.PersonEdit.DataPortalOperations.g.verified.cs b/Source/tests/Csla.Generator.DataPortalInterfaces.CSharp.Tests/Helpers/Snapshots/DataPortalInterfaceGeneratorTests.DeleteWithCriteria#TestApp.PersonEdit.DataPortalOperations.g.verified.cs index 2fe98089d2..c604313f49 100644 --- a/Source/tests/Csla.Generator.DataPortalInterfaces.CSharp.Tests/Helpers/Snapshots/DataPortalInterfaceGeneratorTests.DeleteWithCriteria#TestApp.PersonEdit.DataPortalOperations.g.verified.cs +++ b/Source/tests/Csla.Generator.DataPortalInterfaces.CSharp.Tests/Helpers/Snapshots/DataPortalInterfaceGeneratorTests.DeleteWithCriteria#TestApp.PersonEdit.DataPortalOperations.g.verified.cs @@ -4,7 +4,7 @@ namespace TestApp { - public partial class PersonEdit : Csla.Server.IDataPortalOperationMapping + public partial class PersonEdit : Csla.Server.IDataPortalOperationMapping, Csla.Server.IDataPortalOperationNamedMapping { async global::System.Threading.Tasks.Task Csla.Server.IDataPortalOperationMapping.InvokeOperationAsync( global::System.Type operationType, bool isSync, object?[]? criteria, global::System.IServiceProvider serviceProvider) @@ -19,5 +19,21 @@ public partial class PersonEdit : Csla.Server.IDataPortalOperationMapping } throw new Csla.Server.DataPortalOperationNotSupportedException(operationType, criteria); } + + async global::System.Threading.Tasks.Task Csla.Server.IDataPortalOperationNamedMapping.InvokeNamedOperationAsync( + string operationName, bool isSync, object?[]? criteria, global::System.IServiceProvider serviceProvider) + { + switch (operationName) + { + case "Delete__Int32": + if (criteria is { Length: 1 } && criteria[0] is int p0_int) + { + Delete(p0_int); + return; + } + break; + } + throw new Csla.Server.DataPortalOperationNotSupportedException(operationName, criteria); + } } } diff --git a/Source/tests/Csla.Generator.DataPortalInterfaces.CSharp.Tests/Helpers/Snapshots/DataPortalInterfaceGeneratorTests.ExecuteCommand#TestApp.MyCommand.DataPortalOperations.g.verified.cs b/Source/tests/Csla.Generator.DataPortalInterfaces.CSharp.Tests/Helpers/Snapshots/DataPortalInterfaceGeneratorTests.ExecuteCommand#TestApp.MyCommand.DataPortalOperations.g.verified.cs index fc5fa99695..59e4789567 100644 --- a/Source/tests/Csla.Generator.DataPortalInterfaces.CSharp.Tests/Helpers/Snapshots/DataPortalInterfaceGeneratorTests.ExecuteCommand#TestApp.MyCommand.DataPortalOperations.g.verified.cs +++ b/Source/tests/Csla.Generator.DataPortalInterfaces.CSharp.Tests/Helpers/Snapshots/DataPortalInterfaceGeneratorTests.ExecuteCommand#TestApp.MyCommand.DataPortalOperations.g.verified.cs @@ -4,7 +4,7 @@ namespace TestApp { - public partial class MyCommand : Csla.Server.IDataPortalOperationMapping + public partial class MyCommand : Csla.Server.IDataPortalOperationMapping, Csla.Server.IDataPortalOperationNamedMapping { async global::System.Threading.Tasks.Task Csla.Server.IDataPortalOperationMapping.InvokeOperationAsync( global::System.Type operationType, bool isSync, object?[]? criteria, global::System.IServiceProvider serviceProvider) @@ -19,5 +19,21 @@ public partial class MyCommand : Csla.Server.IDataPortalOperationMapping } throw new Csla.Server.DataPortalOperationNotSupportedException(operationType, criteria); } + + async global::System.Threading.Tasks.Task Csla.Server.IDataPortalOperationNamedMapping.InvokeNamedOperationAsync( + string operationName, bool isSync, object?[]? criteria, global::System.IServiceProvider serviceProvider) + { + switch (operationName) + { + case "Execute": + if (criteria is null or { Length: 0 }) + { + Execute(); + return; + } + break; + } + throw new Csla.Server.DataPortalOperationNotSupportedException(operationName, criteria); + } } } diff --git a/Source/tests/Csla.Generator.DataPortalInterfaces.CSharp.Tests/Helpers/Snapshots/DataPortalInterfaceGeneratorTests.FileScopedNamespace#TestApp.PersonEdit.DataPortalOperations.g.verified.cs b/Source/tests/Csla.Generator.DataPortalInterfaces.CSharp.Tests/Helpers/Snapshots/DataPortalInterfaceGeneratorTests.FileScopedNamespace#TestApp.PersonEdit.DataPortalOperations.g.verified.cs index 2c1504add9..a024ca2cff 100644 --- a/Source/tests/Csla.Generator.DataPortalInterfaces.CSharp.Tests/Helpers/Snapshots/DataPortalInterfaceGeneratorTests.FileScopedNamespace#TestApp.PersonEdit.DataPortalOperations.g.verified.cs +++ b/Source/tests/Csla.Generator.DataPortalInterfaces.CSharp.Tests/Helpers/Snapshots/DataPortalInterfaceGeneratorTests.FileScopedNamespace#TestApp.PersonEdit.DataPortalOperations.g.verified.cs @@ -4,7 +4,7 @@ namespace TestApp { - public partial class PersonEdit : Csla.Server.IDataPortalOperationMapping + public partial class PersonEdit : Csla.Server.IDataPortalOperationMapping, Csla.Server.IDataPortalOperationNamedMapping { async global::System.Threading.Tasks.Task Csla.Server.IDataPortalOperationMapping.InvokeOperationAsync( global::System.Type operationType, bool isSync, object?[]? criteria, global::System.IServiceProvider serviceProvider) @@ -27,5 +27,28 @@ public partial class PersonEdit : Csla.Server.IDataPortalOperationMapping } throw new Csla.Server.DataPortalOperationNotSupportedException(operationType, criteria); } + + async global::System.Threading.Tasks.Task Csla.Server.IDataPortalOperationNamedMapping.InvokeNamedOperationAsync( + string operationName, bool isSync, object?[]? criteria, global::System.IServiceProvider serviceProvider) + { + switch (operationName) + { + case "Create": + if (criteria is null or { Length: 0 }) + { + Create(); + return; + } + break; + case "Fetch__Int32": + if (criteria is { Length: 1 } && criteria[0] is int p0_int) + { + Fetch(p0_int); + return; + } + break; + } + throw new Csla.Server.DataPortalOperationNotSupportedException(operationName, criteria); + } } } diff --git a/Source/tests/Csla.Generator.DataPortalInterfaces.CSharp.Tests/Helpers/Snapshots/DataPortalInterfaceGeneratorTests.MultipleOverloads#TestApp.PersonEdit.DataPortalOperations.g.verified.cs b/Source/tests/Csla.Generator.DataPortalInterfaces.CSharp.Tests/Helpers/Snapshots/DataPortalInterfaceGeneratorTests.MultipleOverloads#TestApp.PersonEdit.DataPortalOperations.g.verified.cs index 76aa316f0f..dd8fac0098 100644 --- a/Source/tests/Csla.Generator.DataPortalInterfaces.CSharp.Tests/Helpers/Snapshots/DataPortalInterfaceGeneratorTests.MultipleOverloads#TestApp.PersonEdit.DataPortalOperations.g.verified.cs +++ b/Source/tests/Csla.Generator.DataPortalInterfaces.CSharp.Tests/Helpers/Snapshots/DataPortalInterfaceGeneratorTests.MultipleOverloads#TestApp.PersonEdit.DataPortalOperations.g.verified.cs @@ -4,7 +4,7 @@ namespace TestApp { - public partial class PersonEdit : Csla.Server.IDataPortalOperationMapping + public partial class PersonEdit : Csla.Server.IDataPortalOperationMapping, Csla.Server.IDataPortalOperationNamedMapping { async global::System.Threading.Tasks.Task Csla.Server.IDataPortalOperationMapping.InvokeOperationAsync( global::System.Type operationType, bool isSync, object?[]? criteria, global::System.IServiceProvider serviceProvider) @@ -24,5 +24,28 @@ public partial class PersonEdit : Csla.Server.IDataPortalOperationMapping } throw new Csla.Server.DataPortalOperationNotSupportedException(operationType, criteria); } + + async global::System.Threading.Tasks.Task Csla.Server.IDataPortalOperationNamedMapping.InvokeNamedOperationAsync( + string operationName, bool isSync, object?[]? criteria, global::System.IServiceProvider serviceProvider) + { + switch (operationName) + { + case "Fetch__String": + if (criteria is { Length: 1 } && criteria[0] is string p0_string) + { + Fetch(p0_string); + return; + } + break; + case "Fetch__Int32": + if (criteria is { Length: 1 } && criteria[0] is int p0_int) + { + Fetch(p0_int); + return; + } + break; + } + throw new Csla.Server.DataPortalOperationNotSupportedException(operationName, criteria); + } } } diff --git a/Source/tests/Csla.Generator.DataPortalInterfaces.CSharp.Tests/Helpers/Snapshots/DataPortalInterfaceGeneratorTests.NestedClass#TestApp.Outer.PersonEdit.DataPortalOperations.g.verified.cs b/Source/tests/Csla.Generator.DataPortalInterfaces.CSharp.Tests/Helpers/Snapshots/DataPortalInterfaceGeneratorTests.NestedClass#TestApp.Outer.PersonEdit.DataPortalOperations.g.verified.cs index a0195aff5a..bb807b6b31 100644 --- a/Source/tests/Csla.Generator.DataPortalInterfaces.CSharp.Tests/Helpers/Snapshots/DataPortalInterfaceGeneratorTests.NestedClass#TestApp.Outer.PersonEdit.DataPortalOperations.g.verified.cs +++ b/Source/tests/Csla.Generator.DataPortalInterfaces.CSharp.Tests/Helpers/Snapshots/DataPortalInterfaceGeneratorTests.NestedClass#TestApp.Outer.PersonEdit.DataPortalOperations.g.verified.cs @@ -6,7 +6,7 @@ namespace TestApp { public partial class Outer { - public partial class PersonEdit : Csla.Server.IDataPortalOperationMapping + public partial class PersonEdit : Csla.Server.IDataPortalOperationMapping, Csla.Server.IDataPortalOperationNamedMapping { async global::System.Threading.Tasks.Task Csla.Server.IDataPortalOperationMapping.InvokeOperationAsync( global::System.Type operationType, bool isSync, object?[]? criteria, global::System.IServiceProvider serviceProvider) @@ -21,6 +21,22 @@ public partial class PersonEdit : Csla.Server.IDataPortalOperationMapping } throw new Csla.Server.DataPortalOperationNotSupportedException(operationType, criteria); } + + async global::System.Threading.Tasks.Task Csla.Server.IDataPortalOperationNamedMapping.InvokeNamedOperationAsync( + string operationName, bool isSync, object?[]? criteria, global::System.IServiceProvider serviceProvider) + { + switch (operationName) + { + case "Fetch__Int32": + if (criteria is { Length: 1 } && criteria[0] is int p0_int) + { + Fetch(p0_int); + return; + } + break; + } + throw new Csla.Server.DataPortalOperationNotSupportedException(operationName, criteria); + } } } } diff --git a/Source/tests/Csla.Generator.DataPortalInterfaces.CSharp.Tests/Helpers/Snapshots/DataPortalInterfaceGeneratorTests.OperationWithInjectAllowNull#TestApp.PersonEdit.DataPortalOperations.g.verified.cs b/Source/tests/Csla.Generator.DataPortalInterfaces.CSharp.Tests/Helpers/Snapshots/DataPortalInterfaceGeneratorTests.OperationWithInjectAllowNull#TestApp.PersonEdit.DataPortalOperations.g.verified.cs index 08e5f11c0b..b8bf49e1d1 100644 --- a/Source/tests/Csla.Generator.DataPortalInterfaces.CSharp.Tests/Helpers/Snapshots/DataPortalInterfaceGeneratorTests.OperationWithInjectAllowNull#TestApp.PersonEdit.DataPortalOperations.g.verified.cs +++ b/Source/tests/Csla.Generator.DataPortalInterfaces.CSharp.Tests/Helpers/Snapshots/DataPortalInterfaceGeneratorTests.OperationWithInjectAllowNull#TestApp.PersonEdit.DataPortalOperations.g.verified.cs @@ -4,7 +4,7 @@ namespace TestApp { - public partial class PersonEdit : Csla.Server.IDataPortalOperationMapping + public partial class PersonEdit : Csla.Server.IDataPortalOperationMapping, Csla.Server.IDataPortalOperationNamedMapping { async global::System.Threading.Tasks.Task Csla.Server.IDataPortalOperationMapping.InvokeOperationAsync( global::System.Type operationType, bool isSync, object?[]? criteria, global::System.IServiceProvider serviceProvider) @@ -20,5 +20,22 @@ public partial class PersonEdit : Csla.Server.IDataPortalOperationMapping } throw new Csla.Server.DataPortalOperationNotSupportedException(operationType, criteria); } + + async global::System.Threading.Tasks.Task Csla.Server.IDataPortalOperationNamedMapping.InvokeNamedOperationAsync( + string operationName, bool isSync, object?[]? criteria, global::System.IServiceProvider serviceProvider) + { + switch (operationName) + { + case "Fetch__Int32": + if (criteria is { Length: 1 } && criteria[0] is int p0_int) + { + var logger = (global::TestApp.ILogger?)serviceProvider.GetService(typeof(global::TestApp.ILogger)); + await Fetch(p0_int, logger).ConfigureAwait(false); + return; + } + break; + } + throw new Csla.Server.DataPortalOperationNotSupportedException(operationName, criteria); + } } } diff --git a/Source/tests/Csla.Generator.DataPortalInterfaces.CSharp.Tests/Helpers/Snapshots/DataPortalInterfaceGeneratorTests.OperationWithInjectParam#TestApp.PersonEdit.DataPortalOperations.g.verified.cs b/Source/tests/Csla.Generator.DataPortalInterfaces.CSharp.Tests/Helpers/Snapshots/DataPortalInterfaceGeneratorTests.OperationWithInjectParam#TestApp.PersonEdit.DataPortalOperations.g.verified.cs index ce52056eae..af73cdb9a5 100644 --- a/Source/tests/Csla.Generator.DataPortalInterfaces.CSharp.Tests/Helpers/Snapshots/DataPortalInterfaceGeneratorTests.OperationWithInjectParam#TestApp.PersonEdit.DataPortalOperations.g.verified.cs +++ b/Source/tests/Csla.Generator.DataPortalInterfaces.CSharp.Tests/Helpers/Snapshots/DataPortalInterfaceGeneratorTests.OperationWithInjectParam#TestApp.PersonEdit.DataPortalOperations.g.verified.cs @@ -4,7 +4,7 @@ namespace TestApp { - public partial class PersonEdit : Csla.Server.IDataPortalOperationMapping + public partial class PersonEdit : Csla.Server.IDataPortalOperationMapping, Csla.Server.IDataPortalOperationNamedMapping { async global::System.Threading.Tasks.Task Csla.Server.IDataPortalOperationMapping.InvokeOperationAsync( global::System.Type operationType, bool isSync, object?[]? criteria, global::System.IServiceProvider serviceProvider) @@ -20,5 +20,22 @@ public partial class PersonEdit : Csla.Server.IDataPortalOperationMapping } throw new Csla.Server.DataPortalOperationNotSupportedException(operationType, criteria); } + + async global::System.Threading.Tasks.Task Csla.Server.IDataPortalOperationNamedMapping.InvokeNamedOperationAsync( + string operationName, bool isSync, object?[]? criteria, global::System.IServiceProvider serviceProvider) + { + switch (operationName) + { + case "Insert": + if (criteria is null or { Length: 0 }) + { + var dal = (global::TestApp.IDal)(serviceProvider.GetService(typeof(global::TestApp.IDal)) ?? throw new global::System.InvalidOperationException($"No service for type '{typeof(global::TestApp.IDal)}' has been registered.")); + await Insert(dal).ConfigureAwait(false); + return; + } + break; + } + throw new Csla.Server.DataPortalOperationNotSupportedException(operationName, criteria); + } } } diff --git a/Source/tests/Csla.Generator.DataPortalInterfaces.CSharp.Tests/Helpers/Snapshots/DataPortalInterfaceGeneratorTests.RootAndChildAttributes#TestApp.PersonEdit.DataPortalOperations.g.verified.cs b/Source/tests/Csla.Generator.DataPortalInterfaces.CSharp.Tests/Helpers/Snapshots/DataPortalInterfaceGeneratorTests.RootAndChildAttributes#TestApp.PersonEdit.DataPortalOperations.g.verified.cs index ff998cc8d0..4826fada4d 100644 --- a/Source/tests/Csla.Generator.DataPortalInterfaces.CSharp.Tests/Helpers/Snapshots/DataPortalInterfaceGeneratorTests.RootAndChildAttributes#TestApp.PersonEdit.DataPortalOperations.g.verified.cs +++ b/Source/tests/Csla.Generator.DataPortalInterfaces.CSharp.Tests/Helpers/Snapshots/DataPortalInterfaceGeneratorTests.RootAndChildAttributes#TestApp.PersonEdit.DataPortalOperations.g.verified.cs @@ -4,7 +4,7 @@ namespace TestApp { - public partial class PersonEdit : Csla.Server.IDataPortalOperationMapping + public partial class PersonEdit : Csla.Server.IDataPortalOperationMapping, Csla.Server.IDataPortalOperationNamedMapping { async global::System.Threading.Tasks.Task Csla.Server.IDataPortalOperationMapping.InvokeOperationAsync( global::System.Type operationType, bool isSync, object?[]? criteria, global::System.IServiceProvider serviceProvider) @@ -27,5 +27,28 @@ public partial class PersonEdit : Csla.Server.IDataPortalOperationMapping } throw new Csla.Server.DataPortalOperationNotSupportedException(operationType, criteria); } + + async global::System.Threading.Tasks.Task Csla.Server.IDataPortalOperationNamedMapping.InvokeNamedOperationAsync( + string operationName, bool isSync, object?[]? criteria, global::System.IServiceProvider serviceProvider) + { + switch (operationName) + { + case "Create": + if (criteria is null or { Length: 0 }) + { + Create(); + return; + } + break; + case "CreateChild": + if (criteria is null or { Length: 0 }) + { + Create(); + return; + } + break; + } + throw new Csla.Server.DataPortalOperationNotSupportedException(operationName, criteria); + } } } diff --git a/Source/tests/Csla.Generator.DataPortalInterfaces.CSharp.Tests/Helpers/Snapshots/DataPortalInterfaceGeneratorTests.SingleCreateNoParams#TestApp.PersonEdit.DataPortalOperations.g.verified.cs b/Source/tests/Csla.Generator.DataPortalInterfaces.CSharp.Tests/Helpers/Snapshots/DataPortalInterfaceGeneratorTests.SingleCreateNoParams#TestApp.PersonEdit.DataPortalOperations.g.verified.cs index d02f5be2ab..19785483bf 100644 --- a/Source/tests/Csla.Generator.DataPortalInterfaces.CSharp.Tests/Helpers/Snapshots/DataPortalInterfaceGeneratorTests.SingleCreateNoParams#TestApp.PersonEdit.DataPortalOperations.g.verified.cs +++ b/Source/tests/Csla.Generator.DataPortalInterfaces.CSharp.Tests/Helpers/Snapshots/DataPortalInterfaceGeneratorTests.SingleCreateNoParams#TestApp.PersonEdit.DataPortalOperations.g.verified.cs @@ -4,7 +4,7 @@ namespace TestApp { - public partial class PersonEdit : Csla.Server.IDataPortalOperationMapping + public partial class PersonEdit : Csla.Server.IDataPortalOperationMapping, Csla.Server.IDataPortalOperationNamedMapping { async global::System.Threading.Tasks.Task Csla.Server.IDataPortalOperationMapping.InvokeOperationAsync( global::System.Type operationType, bool isSync, object?[]? criteria, global::System.IServiceProvider serviceProvider) @@ -19,5 +19,21 @@ public partial class PersonEdit : Csla.Server.IDataPortalOperationMapping } throw new Csla.Server.DataPortalOperationNotSupportedException(operationType, criteria); } + + async global::System.Threading.Tasks.Task Csla.Server.IDataPortalOperationNamedMapping.InvokeNamedOperationAsync( + string operationName, bool isSync, object?[]? criteria, global::System.IServiceProvider serviceProvider) + { + switch (operationName) + { + case "Create": + if (criteria is null or { Length: 0 }) + { + Create(); + return; + } + break; + } + throw new Csla.Server.DataPortalOperationNotSupportedException(operationName, criteria); + } } } diff --git a/Source/tests/Csla.Generator.DataPortalInterfaces.CSharp.Tests/Helpers/Snapshots/DataPortalInterfaceGeneratorTests.SingleFetchWithCriteria#TestApp.PersonEdit.DataPortalOperations.g.verified.cs b/Source/tests/Csla.Generator.DataPortalInterfaces.CSharp.Tests/Helpers/Snapshots/DataPortalInterfaceGeneratorTests.SingleFetchWithCriteria#TestApp.PersonEdit.DataPortalOperations.g.verified.cs index 2df0544dde..3f44ca0017 100644 --- a/Source/tests/Csla.Generator.DataPortalInterfaces.CSharp.Tests/Helpers/Snapshots/DataPortalInterfaceGeneratorTests.SingleFetchWithCriteria#TestApp.PersonEdit.DataPortalOperations.g.verified.cs +++ b/Source/tests/Csla.Generator.DataPortalInterfaces.CSharp.Tests/Helpers/Snapshots/DataPortalInterfaceGeneratorTests.SingleFetchWithCriteria#TestApp.PersonEdit.DataPortalOperations.g.verified.cs @@ -4,7 +4,7 @@ namespace TestApp { - public partial class PersonEdit : Csla.Server.IDataPortalOperationMapping + public partial class PersonEdit : Csla.Server.IDataPortalOperationMapping, Csla.Server.IDataPortalOperationNamedMapping { async global::System.Threading.Tasks.Task Csla.Server.IDataPortalOperationMapping.InvokeOperationAsync( global::System.Type operationType, bool isSync, object?[]? criteria, global::System.IServiceProvider serviceProvider) @@ -19,5 +19,21 @@ public partial class PersonEdit : Csla.Server.IDataPortalOperationMapping } throw new Csla.Server.DataPortalOperationNotSupportedException(operationType, criteria); } + + async global::System.Threading.Tasks.Task Csla.Server.IDataPortalOperationNamedMapping.InvokeNamedOperationAsync( + string operationName, bool isSync, object?[]? criteria, global::System.IServiceProvider serviceProvider) + { + switch (operationName) + { + case "Fetch__Int32": + if (criteria is { Length: 1 } && criteria[0] is int p0_int) + { + Fetch(p0_int); + return; + } + break; + } + throw new Csla.Server.DataPortalOperationNotSupportedException(operationName, criteria); + } } } diff --git a/Source/tests/csla.netcore.test/DataPortal/OperationNameFlowTests.cs b/Source/tests/csla.netcore.test/DataPortal/OperationNameFlowTests.cs new file mode 100644 index 0000000000..ffa810e360 --- /dev/null +++ b/Source/tests/csla.netcore.test/DataPortal/OperationNameFlowTests.cs @@ -0,0 +1,423 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Marimer LLC. All rights reserved. +// Website: https://cslanet.com +// +// Tests for operation name computation and flow +//----------------------------------------------------------------------- +using System.Reflection; +using Csla.Server; +using Csla.TestHelpers; +using FluentAssertions; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Csla.Test.DataPortal +{ + [TestClass] + public class OperationNameFlowTests + { + private static TestDIContext _diContext = default!; + + [ClassInitialize] + public static void ClassSetup(TestContext context) + { + _ = context; + _diContext = TestDIContextFactory.CreateDefaultContext(); + } + + #region Operation Name Computation Tests + + [TestMethod] + public void ComputeOperationName_NoCriteria_ReturnsBaseName() + { + // Arrange + var method = typeof(TestRoot).GetMethod("DataPortal_Fetch", BindingFlags.NonPublic | BindingFlags.Instance, null, Type.EmptyTypes, null); + + // Act + var operationName = DataPortalOperationNameHelper.ComputeOperationName(method!); + + // Assert + operationName.Should().Be("Fetch"); + } + + [TestMethod] + public void ComputeOperationName_SingleIntParameter_ReturnsCorrectName() + { + // Arrange + var method = typeof(TestRoot).GetMethod("DataPortal_Fetch", BindingFlags.NonPublic | BindingFlags.Instance, null, [typeof(int)], null); + + // Act + var operationName = DataPortalOperationNameHelper.ComputeOperationName(method!); + + // Assert + operationName.Should().Be("Fetch__Int32"); + } + + [TestMethod] + public void ComputeOperationName_SingleStringParameter_ReturnsCorrectName() + { + // Arrange + var method = typeof(TestRoot).GetMethod("DataPortal_Fetch", BindingFlags.NonPublic | BindingFlags.Instance, null, [typeof(string)], null); + + // Act + var operationName = DataPortalOperationNameHelper.ComputeOperationName(method!); + + // Assert + operationName.Should().Be("Fetch__String"); + } + + [TestMethod] + public void ComputeOperationName_MultipleParameters_ReturnsCorrectName() + { + // Arrange + var method = typeof(TestRoot).GetMethod("DataPortal_Fetch", BindingFlags.NonPublic | BindingFlags.Instance, null, [typeof(int), typeof(string)], null); + + // Act + var operationName = DataPortalOperationNameHelper.ComputeOperationName(method!); + + // Assert + operationName.Should().Be("Fetch__Int32_String"); + } + + [TestMethod] + public void ComputeOperationName_ArrayParameter_ReturnsCorrectName() + { + // Arrange + var method = typeof(TestRoot).GetMethod("DataPortal_Fetch", BindingFlags.NonPublic | BindingFlags.Instance, null, [typeof(int[])], null); + + // Act + var operationName = DataPortalOperationNameHelper.ComputeOperationName(method!); + + // Assert + operationName.Should().Be("Fetch__Int32Array"); + } + + [TestMethod] + public void ComputeOperationName_GenericParameter_ReturnsCorrectName() + { + // Arrange + var method = typeof(TestRoot).GetMethod("DataPortal_Fetch", BindingFlags.NonPublic | BindingFlags.Instance, null, [typeof(List)], null); + + // Act + var operationName = DataPortalOperationNameHelper.ComputeOperationName(method!); + + // Assert + operationName.Should().Be("Fetch__List_1_Int32"); + } + + [TestMethod] + public void ComputeOperationName_WithInjectAttribute_IgnoresInjectedParameter() + { + // Arrange + var method = typeof(TestRootWithDI).GetMethod("DataPortal_Fetch", BindingFlags.NonPublic | BindingFlags.Instance); + + // Act + var operationName = DataPortalOperationNameHelper.ComputeOperationName(method!); + + // Assert + operationName.Should().Be("Fetch__Int32"); + } + + [TestMethod] + public void ComputeOperationName_CreateOperation_ReturnsCorrectBaseName() + { + // Arrange + var method = typeof(TestRoot).GetMethod("DataPortal_Create", BindingFlags.NonPublic | BindingFlags.Instance, null, [typeof(string)], null); + + // Act + var operationName = DataPortalOperationNameHelper.ComputeOperationName(method!); + + // Assert + operationName.Should().Be("Create__String"); + } + + [TestMethod] + public void ComputeOperationName_DeleteOperation_ReturnsCorrectBaseName() + { + // Arrange + var method = typeof(TestRoot).GetMethod("DataPortal_Delete", BindingFlags.NonPublic | BindingFlags.Instance, null, [typeof(int)], null); + + // Act + var operationName = DataPortalOperationNameHelper.ComputeOperationName(method!); + + // Assert + operationName.Should().Be("Delete__Int32"); + } + + [TestMethod] + public void ComputeOperationName_ExecuteOperation_ReturnsCorrectBaseName() + { + // Arrange + var method = typeof(TestCommand).GetMethod("DataPortal_Execute", BindingFlags.NonPublic | BindingFlags.Instance, null, Type.EmptyTypes, null); + + // Act + var operationName = DataPortalOperationNameHelper.ComputeOperationName(method!); + + // Assert + operationName.Should().Be("Execute"); + } + + #endregion + + #region Integration Tests + + [TestMethod] + public async Task DataPortal_Fetch_FlowsOperationNameToServer() + { + // Arrange & Act + var obj = await _diContext.CreateDataPortal().FetchAsync(123); + + // Assert + obj.Should().NotBeNull(); + obj.OperationNameWasCalled.Should().BeTrue(); + } + + [TestMethod] + public async Task DataPortal_Create_FlowsOperationNameToServer() + { + // Arrange & Act + var obj = await _diContext.CreateDataPortal().CreateAsync("test"); + + // Assert + obj.Should().NotBeNull(); + obj.OperationNameWasCalled.Should().BeTrue(); + } + + [TestMethod] + public async Task DataPortal_Execute_FlowsOperationNameToServer() + { + // Arrange + var dp = _diContext.CreateDataPortal(); + var cmd = await dp.CreateAsync(); + + // Act + cmd = await dp.ExecuteAsync(cmd); + + // Assert + cmd.Should().NotBeNull(); + cmd.OperationNameWasCalled.Should().BeTrue(); + } + + [TestMethod] + public async Task DataPortal_Update_FlowsInsertOperationName() + { + // Arrange + var obj = await _diContext.CreateDataPortal().CreateAsync(); + + // Act + obj = await _diContext.CreateDataPortal().UpdateAsync(obj); + + // Assert + obj.Should().NotBeNull(); + obj.OperationNameWasCalled.Should().BeTrue(); + } + + [TestMethod] + public async Task DataPortal_Update_FlowsUpdateOperationName() + { + // Arrange + var obj = await _diContext.CreateDataPortal().FetchAsync(123); + + // Act + obj = await _diContext.CreateDataPortal().UpdateAsync(obj); + + // Assert + obj.Should().NotBeNull(); + obj.OperationNameWasCalled.Should().BeTrue(); + } + + [TestMethod] + public async Task DataPortal_Update_FlowsDeleteSelfOperationName() + { + // Arrange + var obj = await _diContext.CreateDataPortal().FetchAsync(123); + obj.Delete(); + + // Act + obj = await _diContext.CreateDataPortal().UpdateAsync(obj); + + // Assert + obj.Should().NotBeNull(); + obj.OperationNameWasCalled.Should().BeTrue(); + } + + #endregion + + #region Test Classes + + [Serializable] + public class TestRoot : BusinessBase + { + [Fetch] + private void DataPortal_Fetch() + { + } + + [Fetch] + private void DataPortal_Fetch(int id) + { + } + + [Fetch] + private void DataPortal_Fetch(string name) + { + } + + [Fetch] + private void DataPortal_Fetch(int id, string name) + { + } + + [Fetch] + private void DataPortal_Fetch(int[] ids) + { + } + + [Fetch] + private void DataPortal_Fetch(List ids) + { + } + + [Create] + private void DataPortal_Create(string name) + { + } + + [Delete] + private void DataPortal_Delete(int id) + { + } + } + + [Serializable] + public class TestRootWithDI : BusinessBase + { + [Fetch] + private void DataPortal_Fetch(int id, [Inject] IServiceProvider serviceProvider) + { + } + } + + [Serializable] + public class TestCommand : CommandBase + { + [Execute] + private void DataPortal_Execute() + { + } + } + + [Serializable] + public class TestRootForFlow : BusinessBase, IDataPortalOperationNamedMapping + { + public static readonly PropertyInfo OperationNameWasCalledProperty = RegisterProperty(nameof(OperationNameWasCalled)); + public bool OperationNameWasCalled + { + get => GetProperty(OperationNameWasCalledProperty); + private set => LoadProperty(OperationNameWasCalledProperty, value); + } + + public Task InvokeNamedOperationAsync(string operationName, bool isSync, object?[]? criteria, IServiceProvider serviceProvider) + { + OperationNameWasCalled = true; + + // Dispatch based on operation name + switch (operationName) + { + case "Create": + DataPortal_Create(); + break; + case "Create__String": + if (criteria is { Length: 1 } && criteria[0] is string name) + DataPortal_Create(name); + break; + case "Fetch__Int32": + if (criteria is { Length: 1 } && criteria[0] is int id) + DataPortal_Fetch(id); + break; + case "Insert": + DataPortal_Insert(); + break; + case "Update": + DataPortal_Update(); + break; + case "DeleteSelf": + DataPortal_DeleteSelf(); + break; + default: + throw new DataPortalOperationNotSupportedException(operationName, criteria); + } + + return Task.CompletedTask; + } + + [Create] + private void DataPortal_Create() + { + } + + [Create] + private void DataPortal_Create(string name) + { + } + + [Fetch] + private void DataPortal_Fetch(int id) + { + MarkOld(); + } + + [Insert] + private void DataPortal_Insert() + { + MarkOld(); + } + + [Update] + private void DataPortal_Update() + { + } + + [DeleteSelf] + private void DataPortal_DeleteSelf() + { + } + } + + [Serializable] + public class TestCommandForFlow : CommandBase, IDataPortalOperationNamedMapping + { + public static readonly PropertyInfo OperationNameWasCalledProperty = RegisterProperty(nameof(OperationNameWasCalled)); + public bool OperationNameWasCalled + { + get => ReadProperty(OperationNameWasCalledProperty); + private set => LoadProperty(OperationNameWasCalledProperty, value); + } + + public Task InvokeNamedOperationAsync(string operationName, bool isSync, object?[]? criteria, IServiceProvider serviceProvider) + { + OperationNameWasCalled = true; + + if (operationName == "Execute") + { + DataPortal_Execute(); + return Task.CompletedTask; + } + + throw new DataPortalOperationNotSupportedException(operationName, criteria); + } + + [Create] + private void DataPortal_Create() + { + } + + [Execute] + private void DataPortal_Execute() + { + } + } + + #endregion + } +} diff --git a/docs/design-named-dispatch.md b/docs/design-named-dispatch.md new file mode 100644 index 0000000000..ee9d338bd9 --- /dev/null +++ b/docs/design-named-dispatch.md @@ -0,0 +1,61 @@ +# Design: Name-Based Data Portal Operation Dispatch (#4359) + +## Context + +The source generator for `IDataPortalOperationMapping` is complete - it generates code that dispatches data portal operations via criteria type pattern matching (e.g., `if (criteria is { Length: 1 } && criteria[0] is int p0_int)`). This eliminates reflection on the server. + +The **next step** is to add **name-based dispatch**: the client computes a deterministic operation name (e.g., `"Fetch__Int32"`) from the resolved method's parameter types and sends it through the wire protocol. The server uses this name for direct, unambiguous method dispatch instead of pattern-matching on criteria types. This paves the way for future client-side strongly-typed extension methods. + +**Naming convention**: `{OperationType}` for no-criteria, `{OperationType}__{Type1}_{Type2}` for criteria params. Only criteria params (not `[Inject]` params) contribute to the name. Uses `Type.Name`/`MetadataName` (e.g., `Int32`, `String`). Generic types: replace backtick with underscore + append type args (e.g., `List_1_Int32`). Arrays: element type + `Array` (e.g., `Int32Array`). + +## New Interface + +`IDataPortalOperationNamedMapping` - a separate interface (not modifying existing `IDataPortalOperationMapping`) to maintain backwards compatibility with pre-compiled assemblies: + +```csharp +public interface IDataPortalOperationNamedMapping +{ + Task InvokeNamedOperationAsync(string operationName, bool isSync, object?[]? criteria, IServiceProvider serviceProvider); +} +``` + +## Generator Changes + +The generator produces code implementing both `IDataPortalOperationMapping` and `IDataPortalOperationNamedMapping`. The named dispatch method uses a `switch` statement for O(1) lookup: + +```csharp +async Task Csla.Server.IDataPortalOperationNamedMapping.InvokeNamedOperationAsync( + string operationName, bool isSync, object?[]? criteria, IServiceProvider serviceProvider) +{ + switch (operationName) + { + case "Fetch__Int32": + if (criteria is { Length: 1 } && criteria[0] is int p0_int) + { + Fetch(p0_int); + return; + } + break; + // ... + } + throw new DataPortalOperationNotSupportedException(operationName, criteria); +} +``` + +## Wire Protocol + +- `CriteriaRequest` gains an `OperationName` property (nullable string, backwards compatible) +- `DataPortalContext` gains an `OperationName` property (nullable string, serialized in Get/SetState) + +## Dispatch Order + +**named** -> **criteria-based** -> **reflection** + +## Backwards Compatibility + +| Scenario | Behavior | +|----------|----------| +| Old client -> new server | `OperationName` is null; server skips name dispatch, uses criteria-based | +| New client -> old server | Old `CriteriaRequest` ignores unknown field; server uses criteria-based | +| Local proxy | `DataPortalContext.OperationName` flows directly (no serialization) | +| Pre-compiled libraries | Old code implements only `IDataPortalOperationMapping`; `_namedMapping` is null; criteria-based dispatch used |