CSLA.NET 5.4.2
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;
14using Csla.Properties;
15
16namespace Csla.Reflection
17{
22 public static class ServiceProviderMethodCaller
23 {
24 private static readonly BindingFlags _bindingAttr = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.DeclaredOnly | BindingFlags.DeclaredOnly;
25 private static readonly BindingFlags _factoryBindingAttr = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly;
26 private static readonly ConcurrentDictionary<string, ServiceProviderMethodInfo> _methodCache =
27 new ConcurrentDictionary<string, ServiceProviderMethodInfo>();
28
36 public static ServiceProviderMethodInfo FindDataPortalMethod<T>(object target, object[] criteria)
37 where T : DataPortalOperationAttribute
38 {
39 if (target == null)
40 throw new ArgumentNullException(nameof(target));
41
42 var targetType = target.GetType();
43 return FindDataPortalMethod<T>(targetType, criteria);
44 }
45
54 public static ServiceProviderMethodInfo FindDataPortalMethod<T>(Type targetType, object[] criteria, bool throwOnError = true)
55 where T : DataPortalOperationAttribute
56 {
57 if (targetType == null)
58 throw new ArgumentNullException(nameof(targetType));
59
60 var typeOfOperation = typeof(T);
61
62 if (typeOfOperation == typeof(DeleteSelfAttribute) || typeOfOperation == typeof(DeleteSelfChildAttribute))
63 {
64 criteria = Array.Empty<object>();
65 }
66
67 var cacheKey = GetCacheKeyName(targetType, typeOfOperation, criteria);
68 if (_methodCache.TryGetValue(cacheKey, out ServiceProviderMethodInfo cachedMethod))
69 if (!throwOnError || cachedMethod != null)
70 return cachedMethod;
71
72 var candidates = new List<ScoredMethodInfo>();
73 var factoryInfo = Csla.Server.ObjectFactoryAttribute.GetObjectFactoryAttribute(targetType);
74 if (factoryInfo != null)
75 {
76 var factoryType = Csla.Server.FactoryDataPortal.FactoryLoader.GetFactoryType(factoryInfo.FactoryTypeName);
77 var ftList = new List<System.Reflection.MethodInfo>();
78 var level = 0;
79 while (factoryType != null)
80 {
81 ftList.Clear();
82 if (typeOfOperation == typeof(CreateAttribute))
83 ftList.AddRange(factoryType.GetMethods(_factoryBindingAttr).Where(m => m.Name == factoryInfo.CreateMethodName));
84 else if (typeOfOperation == typeof(FetchAttribute))
85 ftList.AddRange(factoryType.GetMethods(_factoryBindingAttr).Where(m => m.Name == factoryInfo.FetchMethodName));
86 else if (typeOfOperation == typeof(DeleteAttribute))
87 ftList.AddRange(factoryType.GetMethods(_factoryBindingAttr).Where(m => m.Name == factoryInfo.DeleteMethodName));
88 else if (typeOfOperation == typeof(ExecuteAttribute))
89 ftList.AddRange(factoryType.GetMethods(_factoryBindingAttr).Where(m => m.Name == factoryInfo.ExecuteMethodName));
90 else if (typeOfOperation == typeof(CreateChildAttribute))
91 ftList.AddRange(factoryType.GetMethods(_factoryBindingAttr).Where(m => m.Name == "Child_Create"));
92 else
93 ftList.AddRange(factoryType.GetMethods(_factoryBindingAttr).Where(m => m.Name == factoryInfo.UpdateMethodName));
94 factoryType = factoryType.BaseType;
95 candidates.AddRange(ftList.Select(r => new ScoredMethodInfo { MethodInfo = r, Score = level }));
96 level--;
97 }
98 if (!candidates.Any() && typeOfOperation == typeof(CreateChildAttribute))
99 {
100 var ftlist = targetType.GetMethods(_bindingAttr).Where(m => m.Name == "Child_Create");
101 candidates.AddRange(ftList.Select(r => new ScoredMethodInfo { MethodInfo = r, Score = 0 }));
102 }
103 }
104 else // not using factory types
105 {
106 var tt = targetType;
107 var level = 0;
108 while (tt != null)
109 {
110 var ttList = tt.GetMethods(_bindingAttr).Where(m => m.GetCustomAttributes<T>().Any());
111 candidates.AddRange(ttList.Select(r => new ScoredMethodInfo { MethodInfo = r, Score = level }));
112 tt = tt.BaseType;
113 level--;
114 }
115
116 // if no attribute-based methods found, look for legacy methods
117 if (!candidates.Any())
118 {
119 var attributeName = typeOfOperation.Name.Substring(0, typeOfOperation.Name.IndexOf("Attribute"));
120 var methodName = attributeName.Contains("Child") ?
121 "Child_" + attributeName.Substring(0, attributeName.IndexOf("Child")) :
122 "DataPortal_" + attributeName;
123 tt = targetType;
124 level = 0;
125 while (tt != null)
126 {
127 var ttList = tt.GetMethods(_bindingAttr).Where(m => m.Name == methodName);
128 candidates.AddRange(ttList.Select(r => new ScoredMethodInfo { MethodInfo = r, Score = level }));
129 tt = tt.BaseType;
130 level--;
131 }
132 }
133 }
134
135 ScoredMethodInfo result = null;
136
137 if (candidates != null && candidates.Any())
138 {
139 // scan candidate methods for matching criteria parameters
140 int criteriaLength = 0;
141 if (criteria != null)
142 if (criteria.GetType().Equals(typeof(object[])))
143 criteriaLength = criteria.GetLength(0);
144 else
145 criteriaLength = 1;
146
147 var matches = new List<ScoredMethodInfo>();
148 if (criteriaLength > 0)
149 {
150 foreach (var item in candidates)
151 {
152 int score = 0;
153 var methodParams = GetCriteriaParameters(item.MethodInfo);
154 if (methodParams.Length == criteriaLength)
155 {
156 var index = 0;
157 if (criteria.GetType().Equals(typeof(object[])))
158 {
159 foreach (var c in criteria)
160 {
161 var currentScore = CalculateParameterScore(methodParams[index], c);
162 if (currentScore == 0)
163 {
164 break;
165 }
166
167 score += currentScore;
168 index++;
169 }
170 }
171 else
172 {
173 var currentScore = CalculateParameterScore(methodParams[index], criteria);
174 if (currentScore != 0)
175 {
176 score += currentScore;
177 index++;
178 }
179 }
180
181 if (index == criteriaLength)
182 matches.Add(new ScoredMethodInfo { MethodInfo = item.MethodInfo, Score = score + item.Score });
183 }
184 }
185 }
186 else
187 {
188 foreach (var item in candidates)
189 {
190 if (GetCriteriaParameters(item.MethodInfo).Length == 0)
191 matches.Add(new ScoredMethodInfo { MethodInfo = item.MethodInfo, Score = item.Score });
192 }
193 }
194 if (matches.Count == 0)
195 {
196 // look for params array
197 foreach (var item in candidates)
198 {
199 var lastParam = item.MethodInfo.GetParameters().LastOrDefault();
200 if (lastParam != null && lastParam.ParameterType.Equals(typeof(object[])) &&
201 lastParam.GetCustomAttributes<ParamArrayAttribute>().Any())
202 {
203 matches.Add(new ScoredMethodInfo { MethodInfo = item.MethodInfo, Score = 1 + item.Score });
204 }
205 }
206 }
207
208 if (matches.Count > 0)
209 {
210 result = matches[0];
211
212 if (matches.Count > 1)
213 {
214 // disambiguate if necessary, using a greedy algorithm
215 // so more DI parameters are better
216 foreach (var item in matches)
217 item.Score += GetDIParameters(item.MethodInfo).Length;
218
219 var maxScore = int.MinValue;
220 var maxCount = 0;
221 foreach (var item in matches)
222 {
223 if (item.Score > maxScore)
224 {
225 maxScore = item.Score;
226 maxCount = 1;
227 result = item;
228 }
229 else if (item.Score == maxScore)
230 {
231 maxCount++;
232 }
233 }
234 if (maxCount > 1)
235 {
236 if (throwOnError)
237 {
238 throw new AmbiguousMatchException($"{targetType.FullName}.[{typeOfOperation.Name.Replace("Attribute", "")}]{GetCriteriaTypeNames(criteria)}. Matches: {string.Join(", ", matches.Select(m => $"{m.MethodInfo.DeclaringType.FullName}[{m.MethodInfo}]"))}");
239 }
240 else
241 {
242 _methodCache.TryAdd(cacheKey, null);
243 return null;
244 }
245 }
246 }
247 }
248 }
249
250 ServiceProviderMethodInfo resultingMethod = null;
251 if (result != null)
252 {
253 resultingMethod = new ServiceProviderMethodInfo { MethodInfo = result.MethodInfo };
254 }
255 else
256 {
257 var baseType = targetType.BaseType;
258 if (baseType == null)
259 {
260 if (throwOnError)
261 throw new TargetParameterCountException(cacheKey);
262 else
263 {
264 _methodCache.TryAdd(cacheKey, null);
265 return null;
266 }
267 }
268 try
269 {
270 resultingMethod = FindDataPortalMethod<T>(baseType, criteria, throwOnError);
271 }
272 catch (TargetParameterCountException ex)
273 {
274 throw new TargetParameterCountException(cacheKey, ex);
275 }
276 catch (AmbiguousMatchException ex)
277 {
278 throw new AmbiguousMatchException($"{targetType.FullName}.[{typeOfOperation.Name.Replace("Attribute", "")}]{GetCriteriaTypeNames(criteria)}.", ex);
279 }
280 }
281 _methodCache.TryAdd(cacheKey, resultingMethod);
282 return resultingMethod;
283 }
284
285 private static int CalculateParameterScore(ParameterInfo methodParam, object c)
286 {
287 if (c == null)
288 {
289 if (methodParam.ParameterType.IsPrimitive)
290 return 0;
291 else if (methodParam.ParameterType == typeof(object))
292 return 2;
293 else if (methodParam.ParameterType == typeof(object[]))
294 return 2;
295 else if (methodParam.ParameterType.IsClass)
296 return 1;
297 else if (methodParam.ParameterType.IsArray)
298 return 1;
299 else if (methodParam.ParameterType.IsInterface)
300 return 1;
301 else if (Nullable.GetUnderlyingType(methodParam.ParameterType) != null)
302 return 2;
303 }
304 else
305 {
306 if (c.GetType() == methodParam.ParameterType)
307 return 3;
308 else if (methodParam.ParameterType.Equals(typeof(object)))
309 return 1;
310 else if (methodParam.ParameterType.IsAssignableFrom(c.GetType()))
311 return 2;
312 }
313
314 return 0;
315 }
316
317 private static string GetCacheKeyName(Type targetType, Type operationType, object[] criteria)
318 {
319 return $"{targetType.FullName}.[{operationType.Name.Replace("Attribute", "")}]{GetCriteriaTypeNames(criteria)}";
320 }
321
322 private static string GetCriteriaTypeNames(object[] criteria)
323 {
324 var result = new System.Text.StringBuilder();
325 result.Append("(");
326 if (criteria != null)
327 {
328 if (criteria.GetType().Equals(typeof(object[])))
329 {
330 bool first = true;
331 foreach (var item in criteria)
332 {
333 if (first)
334 first = false;
335 else
336 result.Append(",");
337 if (item == null)
338 result.Append("null");
339 else
340 result.Append(GetTypeName(item.GetType()));
341 }
342 }
343 else
344 result.Append(GetTypeName(criteria.GetType()));
345 }
346
347 result.Append(")");
348 return result.ToString();
349 }
350
351 private static string GetTypeName(Type type)
352 {
353 if (type.IsArray)
354 {
355 return $"{GetTypeName(type.GetElementType())}[]";
356 }
357
358 if (!type.IsGenericType)
359 {
360 return type.Name;
361 }
362
363 var result = new System.Text.StringBuilder();
364 var genericArguments = type.GetGenericArguments();
365 result.Append(type.Name);
366 result.Append("<");
367
368 for (int i = 0; i < genericArguments.Length; i++)
369 {
370 if (i > 0)
371 {
372 result.Append(",");
373 }
374
375 result.Append(GetTypeName(genericArguments[i]));
376 }
377
378 result.Append(">");
379
380 return result.ToString();
381 }
382
383 private static ParameterInfo[] GetCriteriaParameters(System.Reflection.MethodInfo method)
384 {
385 var result = new List<ParameterInfo>();
386 foreach (var item in method.GetParameters())
387 {
388 if (!item.GetCustomAttributes<InjectAttribute>().Any())
389 result.Add(item);
390 }
391 return result.ToArray();
392 }
393
394 private static ParameterInfo[] GetDIParameters(System.Reflection.MethodInfo method)
395 {
396 var result = new List<ParameterInfo>();
397 foreach (var item in method.GetParameters())
398 {
399 if (item.GetCustomAttributes<InjectAttribute>().Any())
400 result.Add(item);
401 }
402 return result.ToArray();
403 }
404
413 public static async Task<object> CallMethodTryAsync(object obj, ServiceProviderMethodInfo method, object[] parameters)
414 {
415 if (method == null)
416 throw new ArgumentNullException(obj.GetType().FullName + ".<null>() " + Resources.MethodNotImplemented);
417
418 var info = method.MethodInfo;
419 method.PrepForInvocation();
420
421 object[] plist;
422
423 if (method.TakesParamArray)
424 {
425 plist = new object[] { parameters };
426 }
427 else
428 {
429 plist = new object[method.Parameters.Length];
430 int index = 0;
431 int criteriaIndex = 0;
432 var service = ApplicationContext.CurrentServiceProvider;
433 foreach (var item in method.Parameters)
434 {
435 if (method.IsInjected[index])
436 {
437 if (service == null)
438 {
439 throw new NullReferenceException(nameof(service));
440 }
441 plist[index] = service.GetService(item.ParameterType);
442
443 }
444 else
445 {
446 if (parameters.GetType().Equals(typeof(object[])))
447 {
448 if (parameters == null || parameters.Length - 1 < criteriaIndex)
449 plist[index] = null;
450 else
451 plist[index] = parameters[criteriaIndex];
452 }
453 else
454 plist[index] = parameters;
455 criteriaIndex++;
456 }
457 index++;
458 }
459 }
460
461 try
462 {
463 if (method.IsAsyncTask)
464 {
465 await ((Task)method.DynamicMethod(obj, plist)).ConfigureAwait(false);
466 return null;
467 }
468 else if (method.IsAsyncTaskObject)
469 {
470 return await ((Task<object>)method.DynamicMethod(obj, plist)).ConfigureAwait(false);
471 }
472 else
473 {
474 var result = method.DynamicMethod(obj, plist);
475 return result;
476 }
477 }
478 catch (Exception ex)
479 {
480 Exception inner = null;
481 if (ex.InnerException == null)
482 inner = ex;
483 else
484 inner = ex.InnerException;
485 throw new CallMethodException(obj.GetType().Name + "." + info.Name + " " + Resources.MethodCallFailed, inner);
486 }
487 }
488
489 private class ScoredMethodInfo
490 {
491 public int Score { get; set; }
492 public System.Reflection.MethodInfo MethodInfo { get; set; }
493 }
494 }
495}
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.
Server-side data portal implementation that invokes an object factory rather than directly interactin...
static IObjectFactoryLoader FactoryLoader
Gets or sets a delegate reference to the method called to create instances of factory objects as requ...
Specifies that the data portal should invoke a factory object rather than the business object.
Type GetFactoryType(string factoryName)
Returns the type of the factory object.