CSLA.NET 6.0.0
CSLA .NET is a software development framework that helps you build a reusable, maintainable object-oriented business layer for your app.
ServiceProviderMethodCaller.cs
Go to the documentation of this file.
1//-----------------------------------------------------------------------
2// <copyright file="ServiceProviderMethodCaller.cs" company="Marimer LLC">
3// Copyright (c) Marimer LLC. All rights reserved.
4// Website: https://cslanet.com
5// </copyright>
6// <summary>Dynamically find/invoke methods with DI provided params</summary>
7//-----------------------------------------------------------------------
8using System;
9using System.Collections.Concurrent;
10using System.Collections.Generic;
11using System.Linq;
12using System.Reflection;
13using System.Threading.Tasks;
14#if NET5_0_OR_GREATER
15using System.Runtime.Loader;
16
17using Csla.Runtime;
18#endif
19using Csla.Properties;
20
21namespace Csla.Reflection
22{
27 public class ServiceProviderMethodCaller : Core.IUseApplicationContext
28 {
29 private static readonly BindingFlags _bindingAttr = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.DeclaredOnly | BindingFlags.DeclaredOnly;
30 private static readonly BindingFlags _factoryBindingAttr = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly;
31
32#if NET5_0_OR_GREATER
33 private static readonly ConcurrentDictionary<string, Tuple<string, ServiceProviderMethodInfo>> _methodCache =
34 new ConcurrentDictionary<string, Tuple<string, ServiceProviderMethodInfo>>();
35#else
36 private static readonly ConcurrentDictionary<string, ServiceProviderMethodInfo> _methodCache =
37 new ConcurrentDictionary<string, ServiceProviderMethodInfo>();
38#endif
39
40 ApplicationContext Core.IUseApplicationContext.ApplicationContext { get => ApplicationContext; set => ApplicationContext = value; }
41 private ApplicationContext ApplicationContext { get; set; }
42
50 public ServiceProviderMethodInfo FindDataPortalMethod<T>(object target, object[] criteria)
52 {
53 if (target == null)
54 throw new ArgumentNullException(nameof(target));
55
56 var targetType = target.GetType();
57 return FindDataPortalMethod<T>(targetType, criteria);
58 }
59
68 public ServiceProviderMethodInfo FindDataPortalMethod<T>(Type targetType, object[] criteria, bool throwOnError = true)
70 {
71 if (targetType == null)
72 throw new ArgumentNullException(nameof(targetType));
73
74 var typeOfOperation = typeof(T);
75
76 var cacheKey = GetCacheKeyName(targetType, typeOfOperation, criteria);
77
78#if NET5_0_OR_GREATER
79 if (_methodCache.TryGetValue(cacheKey, out var unloadableCachedMethodInfo))
80 {
81 var cachedMethod = unloadableCachedMethodInfo?.Item2;
82
83#else
84 if (_methodCache.TryGetValue(cacheKey, out ServiceProviderMethodInfo cachedMethod))
85 {
86#endif
87 if (!throwOnError || cachedMethod != null)
88 return cachedMethod;
89 }
90
91 var candidates = new List<ScoredMethodInfo>();
92 var factoryInfo = Csla.Server.ObjectFactoryAttribute.GetObjectFactoryAttribute(targetType);
93 if (factoryInfo != null)
94 {
95 var factoryLoader = ApplicationContext.CurrentServiceProvider.GetService(typeof(Server.IObjectFactoryLoader)) as Server.IObjectFactoryLoader;
96 var factoryType = factoryLoader?.GetFactoryType(factoryInfo.FactoryTypeName);
97 var ftList = new List<System.Reflection.MethodInfo>();
98 var level = 0;
99 while (factoryType != null)
100 {
101 ftList.Clear();
102 if (typeOfOperation == typeof(CreateAttribute))
103 ftList.AddRange(factoryType.GetMethods(_factoryBindingAttr).Where(m => m.Name == factoryInfo.CreateMethodName));
104 else if (typeOfOperation == typeof(FetchAttribute))
105 ftList.AddRange(factoryType.GetMethods(_factoryBindingAttr).Where(m => m.Name == factoryInfo.FetchMethodName));
106 else if (typeOfOperation == typeof(DeleteAttribute))
107 ftList.AddRange(factoryType.GetMethods(_factoryBindingAttr).Where(m => m.Name == factoryInfo.DeleteMethodName));
108 else if (typeOfOperation == typeof(ExecuteAttribute))
109 ftList.AddRange(factoryType.GetMethods(_factoryBindingAttr).Where(m => m.Name == factoryInfo.ExecuteMethodName));
110 else if (typeOfOperation == typeof(CreateChildAttribute))
111 ftList.AddRange(factoryType.GetMethods(_factoryBindingAttr).Where(m => m.Name == "Child_Create"));
112 else
113 ftList.AddRange(factoryType.GetMethods(_factoryBindingAttr).Where(m => m.Name == factoryInfo.UpdateMethodName));
114 factoryType = factoryType.BaseType;
115 candidates.AddRange(ftList.Select(r => new ScoredMethodInfo { MethodInfo = r, Score = level }));
116 level--;
117 }
118 if (!candidates.Any() && typeOfOperation == typeof(CreateChildAttribute))
119 {
120 var ftlist = targetType.GetMethods(_bindingAttr).Where(m => m.Name == "Child_Create");
121 candidates.AddRange(ftlist.Select(r => new ScoredMethodInfo { MethodInfo = r, Score = 0 }));
122 }
123 }
124 else // not using factory types
125 {
126 var tt = targetType;
127 var level = 0;
128 while (tt != null)
129 {
130 var ttList = tt.GetMethods(_bindingAttr).Where(m => m.GetCustomAttributes<T>().Any());
131 candidates.AddRange(ttList.Select(r => new ScoredMethodInfo { MethodInfo = r, Score = level }));
132 tt = tt.BaseType;
133 level--;
134 }
135
136 // if no attribute-based methods found, look for legacy methods
137 if (!candidates.Any())
138 {
139 var attributeName = typeOfOperation.Name.Substring(0, typeOfOperation.Name.IndexOf("Attribute"));
140 var methodName = attributeName.Contains("Child") ?
141 "Child_" + attributeName.Substring(0, attributeName.IndexOf("Child")) :
142 "DataPortal_" + attributeName;
143 tt = targetType;
144 level = 0;
145 while (tt != null)
146 {
147 var ttList = tt.GetMethods(_bindingAttr).Where(m => m.Name == methodName);
148 candidates.AddRange(ttList.Select(r => new ScoredMethodInfo { MethodInfo = r, Score = level }));
149 tt = tt.BaseType;
150 level--;
151 }
152 }
153 }
154
155 ScoredMethodInfo result = null;
156
157 if (candidates != null && candidates.Any())
158 {
159 // scan candidate methods for matching criteria parameters
160 int criteriaLength = 0;
161 if (criteria != null)
162 if (criteria.GetType().Equals(typeof(object[])))
163 criteriaLength = criteria.GetLength(0);
164 else
165 criteriaLength = 1;
166
167 var matches = new List<ScoredMethodInfo>();
168 if (criteriaLength > 0)
169 {
170 foreach (var item in candidates)
171 {
172 int score = 0;
173 var methodParams = GetCriteriaParameters(item.MethodInfo);
174 if (methodParams.Length == criteriaLength)
175 {
176 var index = 0;
177 if (criteria.GetType().Equals(typeof(object[])))
178 {
179 foreach (var c in criteria)
180 {
181 var currentScore = CalculateParameterScore(methodParams[index], c);
182 if (currentScore == 0)
183 {
184 break;
185 }
186
187 score += currentScore;
188 index++;
189 }
190 }
191 else
192 {
193 var currentScore = CalculateParameterScore(methodParams[index], criteria);
194 if (currentScore != 0)
195 {
196 score += currentScore;
197 index++;
198 }
199 }
200
201 if (index == criteriaLength)
202 matches.Add(new ScoredMethodInfo { MethodInfo = item.MethodInfo, Score = score + item.Score });
203 }
204 }
205 if (matches.Count() == 0 &&
206 (typeOfOperation == typeof(DeleteSelfAttribute) || typeOfOperation == typeof(DeleteSelfChildAttribute)
207 || typeOfOperation == typeof(UpdateChildAttribute)))
208 {
209 // implement zero parameter fallback if other matches not found
210 foreach (var item in candidates)
211 {
212 if (GetCriteriaParameters(item.MethodInfo).Length == 0)
213 matches.Add(new ScoredMethodInfo { MethodInfo = item.MethodInfo, Score = item.Score });
214 }
215 }
216 }
217 else
218 {
219 foreach (var item in candidates)
220 {
221 if (GetCriteriaParameters(item.MethodInfo).Length == 0)
222 matches.Add(new ScoredMethodInfo { MethodInfo = item.MethodInfo, Score = item.Score });
223 }
224 }
225 if (matches.Count == 0)
226 {
227 // look for params array
228 foreach (var item in candidates)
229 {
230 var lastParam = item.MethodInfo.GetParameters().LastOrDefault();
231 if (lastParam != null && lastParam.ParameterType.Equals(typeof(object[])) &&
232 lastParam.GetCustomAttributes<ParamArrayAttribute>().Any())
233 {
234 matches.Add(new ScoredMethodInfo { MethodInfo = item.MethodInfo, Score = 1 + item.Score });
235 }
236 }
237 }
238
239 if (matches.Count > 0)
240 {
241 result = matches[0];
242
243 if (matches.Count > 1)
244 {
245 // disambiguate if necessary, using a greedy algorithm
246 // so more DI parameters are better
247 foreach (var item in matches)
248 item.Score += GetDIParameters(item.MethodInfo).Length;
249
250 var maxScore = int.MinValue;
251 var maxCount = 0;
252 foreach (var item in matches)
253 {
254 if (item.Score > maxScore)
255 {
256 maxScore = item.Score;
257 maxCount = 1;
258 result = item;
259 }
260 else if (item.Score == maxScore)
261 {
262 maxCount++;
263 }
264 }
265 if (maxCount > 1)
266 {
267 if (throwOnError)
268 {
269 throw new AmbiguousMatchException($"{targetType.FullName}.[{typeOfOperation.Name.Replace("Attribute", "")}]{GetCriteriaTypeNames(criteria)}. Matches: {string.Join(", ", matches.Select(m => $"{m.MethodInfo.DeclaringType.FullName}[{m.MethodInfo}]"))}");
270 }
271 else
272 {
273 _methodCache.TryAdd(cacheKey, null);
274
275 return null;
276 }
277 }
278 }
279 }
280 }
281
282 ServiceProviderMethodInfo resultingMethod = null;
283 if (result != null)
284 {
285 resultingMethod = new ServiceProviderMethodInfo { MethodInfo = result.MethodInfo };
286 }
287 else
288 {
289 var baseType = targetType.BaseType;
290 if (baseType == null)
291 {
292 if (throwOnError)
293 throw new TargetParameterCountException(cacheKey);
294 else
295 {
296 _methodCache.TryAdd(cacheKey, null);
297
298 return null;
299 }
300 }
301 try
302 {
303 resultingMethod = FindDataPortalMethod<T>(baseType, criteria, throwOnError);
304 }
305 catch (TargetParameterCountException ex)
306 {
307 throw new TargetParameterCountException(cacheKey, ex);
308 }
309 catch (AmbiguousMatchException ex)
310 {
311 throw new AmbiguousMatchException($"{targetType.FullName}.[{typeOfOperation.Name.Replace("Attribute", "")}]{GetCriteriaTypeNames(criteria)}.", ex);
312 }
313 }
314
315 if (resultingMethod != null)
316 {
317#if NET5_0_OR_GREATER
318 var cacheInstance = AssemblyLoadContextManager.CreateCacheInstance(targetType, resultingMethod, OnAssemblyLoadContextUnload);
319 _ = _methodCache.TryAdd(cacheKey, cacheInstance);
320#else
321 _methodCache.TryAdd(cacheKey, resultingMethod);
322#endif
323 }
324 return resultingMethod;
325 }
326
327 private static int CalculateParameterScore(ParameterInfo methodParam, object c)
328 {
329 if (c == null)
330 {
331 if (methodParam.ParameterType.IsPrimitive)
332 return 0;
333 else if (methodParam.ParameterType == typeof(object))
334 return 2;
335 else if (methodParam.ParameterType == typeof(object[]))
336 return 2;
337 else if (methodParam.ParameterType.IsClass)
338 return 1;
339 else if (methodParam.ParameterType.IsArray)
340 return 1;
341 else if (methodParam.ParameterType.IsInterface)
342 return 1;
343 else if (Nullable.GetUnderlyingType(methodParam.ParameterType) != null)
344 return 2;
345 }
346 else
347 {
348 if (c.GetType() == methodParam.ParameterType)
349 return 3;
350 else if (methodParam.ParameterType.Equals(typeof(object)))
351 return 1;
352 else if (methodParam.ParameterType.IsAssignableFrom(c.GetType()))
353 return 2;
354 }
355
356 return 0;
357 }
358
359 private static string GetCacheKeyName(Type targetType, Type operationType, object[] criteria)
360 {
361 return $"{targetType.FullName}.[{operationType.Name.Replace("Attribute", "")}]{GetCriteriaTypeNames(criteria)}";
362 }
363
364 private static string GetCriteriaTypeNames(object[] criteria)
365 {
366 var result = new System.Text.StringBuilder();
367 result.Append("(");
368 if (criteria != null)
369 {
370 if (criteria.GetType().Equals(typeof(object[])))
371 {
372 bool first = true;
373 foreach (var item in criteria)
374 {
375 if (first)
376 first = false;
377 else
378 result.Append(",");
379 if (item == null)
380 result.Append("null");
381 else
382 result.Append(GetTypeName(item.GetType()));
383 }
384 }
385 else
386 result.Append(GetTypeName(criteria.GetType()));
387 }
388
389 result.Append(")");
390 return result.ToString();
391 }
392
393 private static string GetTypeName(Type type)
394 {
395 if (type.IsArray)
396 {
397 return $"{GetTypeName(type.GetElementType())}[]";
398 }
399
400 if (!type.IsGenericType)
401 {
402 return type.Name;
403 }
404
405 var result = new System.Text.StringBuilder();
406 var genericArguments = type.GetGenericArguments();
407 result.Append(type.Name);
408 result.Append("<");
409
410 for (int i = 0; i < genericArguments.Length; i++)
411 {
412 if (i > 0)
413 {
414 result.Append(",");
415 }
416
417 result.Append(GetTypeName(genericArguments[i]));
418 }
419
420 result.Append(">");
421
422 return result.ToString();
423 }
424
425 private static ParameterInfo[] GetCriteriaParameters(System.Reflection.MethodInfo method)
426 {
427 var result = new List<ParameterInfo>();
428 foreach (var item in method.GetParameters())
429 {
430 if (!item.GetCustomAttributes<InjectAttribute>().Any())
431 result.Add(item);
432 }
433 return result.ToArray();
434 }
435
436 private static ParameterInfo[] GetDIParameters(System.Reflection.MethodInfo method)
437 {
438 var result = new List<ParameterInfo>();
439 foreach (var item in method.GetParameters())
440 {
441 if (item.GetCustomAttributes<InjectAttribute>().Any())
442 result.Add(item);
443 }
444 return result.ToArray();
445 }
446
455 public async Task<object> CallMethodTryAsync(object obj, ServiceProviderMethodInfo method, object[] parameters)
456 {
457 if (method == null)
458 throw new ArgumentNullException(obj.GetType().FullName + ".<null>() " + Resources.MethodNotImplemented);
459
460 var info = method.MethodInfo;
461 method.PrepForInvocation();
462
463 object[] plist;
464
465 if (method.TakesParamArray)
466 {
467 plist = new object[] { parameters };
468 }
469 else
470 {
471 plist = new object[method.Parameters.Length];
472 int index = 0;
473 int criteriaIndex = 0;
474 var service = ApplicationContext.CurrentServiceProvider;
475 foreach (var item in method.Parameters)
476 {
477 if (method.IsInjected[index])
478 {
479 if (service == null)
480 {
481 throw new NullReferenceException(nameof(service));
482 }
483 plist[index] = service.GetService(item.ParameterType);
484
485 }
486 else
487 {
488 if (parameters.GetType().Equals(typeof(object[])))
489 {
490 if (parameters == null || parameters.Length - 1 < criteriaIndex)
491 plist[index] = null;
492 else
493 plist[index] = parameters[criteriaIndex];
494 }
495 else
496 plist[index] = parameters;
497 criteriaIndex++;
498 }
499 index++;
500 }
501 }
502
503 try
504 {
505 if (method.IsAsyncTask)
506 {
507 await ((Task)method.DynamicMethod(obj, plist)).ConfigureAwait(false);
508 return null;
509 }
510 else if (method.IsAsyncTaskObject)
511 {
512 return await ((Task<object>)method.DynamicMethod(obj, plist)).ConfigureAwait(false);
513 }
514 else
515 {
516 var result = method.DynamicMethod(obj, plist);
517 return result;
518 }
519 }
520 catch (Exception ex)
521 {
522 Exception inner = null;
523 if (ex.InnerException == null)
524 inner = ex;
525 else
526 inner = ex.InnerException;
527 throw new CallMethodException(obj.GetType().Name + "." + info.Name + " " + Resources.MethodCallFailed, inner);
528 }
529 }
530#if NET5_0_OR_GREATER
531
532 private static void OnAssemblyLoadContextUnload(AssemblyLoadContext context)
533 {
534 AssemblyLoadContextManager.RemoveFromCache(_methodCache, context, true);
535 }
536#endif
537
538 private class ScoredMethodInfo
539 {
540 public int Score { get; set; }
541 public System.Reflection.MethodInfo MethodInfo { get; set; }
542 }
543 }
544}
Provides consistent context information between the client and server DataPortal objects.
ApplicationContext(ApplicationContextAccessor applicationContextAccessor)
Creates a new instance of the type
Specifies a method used by the server-side data portal to initialize a new domain object.
Specifies a method used by the server-side data portal to initialize a new child object.
Base type for data portal operation attributes.
Specifies a method used by the server-side data portal to delete domain object data during an update ...
Specifies a method used by the server-side data portal to delete domain object data during an explici...
Specifies a method used by the server-side data portal to delete child object data during an update o...
Specifies a method used by the server-side data portal to execute a command object.
Specifies a method used by the server-side data portal to load existing data into the domain object.
Specifies a parameter that is provided via dependency injection.
Maintains metadata about a method.
Definition: MethodInfo.cs:19
MethodInfo(string name)
Creates an instance of the type.
Definition: MethodInfo.cs:24
A strongly-typed resource class, for looking up localized strings, etc.
static string MethodNotImplemented
Looks up a localized string similar to not implemented.
static string MethodCallFailed
Looks up a localized string similar to method call failed.
This exception is returned from the CallMethod method in the server-side DataPortal and contains the ...
Methods to dynamically find/invoke methods with data portal and DI provided params
async Task< object > CallMethodTryAsync(object obj, ServiceProviderMethodInfo method, object[] parameters)
Invoke a method async if possible, providing parameters from the params array and from DI
ServiceProviderMethodInfo FindDataPortalMethod< T >(object target, object[] criteria)
Find a method based on data portal criteria and providing any remaining parameters with values from a...
Class that contains cached metadata about data portal method to be invoked
System.Reflection.MethodInfo MethodInfo
Gets or sets the MethodInfo object for the method
bool IsAsyncTaskObject
Gets a value indicating whether the method returns a Task of T
bool TakesParamArray
Gets a value indicating whether the method takes a param array as its parameter
void PrepForInvocation()
Initializes and caches the metastate values necessary to invoke the method
DynamicMethodDelegate DynamicMethod
Gets delegate representing an expression that can invoke the method
bool[] IsInjected
Gets an array of values indicating which parameters need to be injected
ParameterInfo[] Parameters
Gets the parameters for the method
bool IsAsyncTask
Gets a value indicating whether the method returns type Task
Specifies that the data portal should invoke a factory object rather than the business object.
Specifies a method used by the server-side data portal to update child object data during an update o...