4 Commits

Author SHA1 Message Date
Claire Kuang 739befc3fa cleans and improves logic to handle no appid objects
build and deploy Speckle functions / publish-automate-function-version (push) Has been cancelled
also changes assumptions that in a single commit, multiple objects can have the same appid and same speckle id
2024-02-02 01:58:54 +00:00
Claire Kuang 5ed6ab2f69 attach warning test
build and deploy Speckle functions / publish-automate-function-version (push) Has been cancelled
2024-02-01 17:25:12 +00:00
Claire Kuang 470b3d93de adds additional information to run failed log 2024-02-01 11:13:55 +00:00
Claire Kuang b9059e1b85 Update AutomateFunction.cs
build and deploy Speckle functions / publish-automate-function-version (push) Has been cancelled
2024-02-01 10:20:36 +00:00
3 changed files with 477 additions and 196 deletions
+201 -195
View File
@@ -1,17 +1,15 @@
using Objects; using Objects;
using Speckle.Automate.Sdk; using Speckle.Automate.Sdk;
using Speckle.Automate.Sdk.Schema;
using Speckle.Core.Api; using Speckle.Core.Api;
using Speckle.Core.Credentials;
using Speckle.Core.Models; using Speckle.Core.Models;
using Speckle.Core.Models.Extensions; using SpeckleAutomateDotnetExample;
using Speckle.Core.Transports;
static class AutomateFunction static class AutomateFunction
{ {
public static string ADDED = "Added"; public static string ADDED = "ADDED";
public static string MODIFIED = "Modified"; public static string MODIFIED = "MODIFIED";
public static string DELETED = "Deleted"; public static string DELETED = "DELETED";
public static string UNCHANGED = "UNCHANGED";
public static async Task Run( public static async Task Run(
AutomationContext automationContext, AutomationContext automationContext,
@@ -21,9 +19,12 @@ static class AutomateFunction
Console.WriteLine("Starting execution"); Console.WriteLine("Starting execution");
_ = typeof(ObjectsKit).Assembly; // INFO: Force objects kit to initialize _ = typeof(ObjectsKit).Assembly; // INFO: Force objects kit to initialize
// get the test and release branch name double tolerance = functionInputs.Tolerance;
var testBranchName = automationContext.AutomationRunData.BranchName;
var releaseBranchName = testBranchName.Replace("/testing", "/release"); // get the testing and release branches
string testBranchName = automationContext.AutomationRunData.BranchName;
string releaseBranchName = testBranchName.Replace("/testing", "/release");
Console.WriteLine($"Comparing {testBranchName} against {releaseBranchName}");
Branch? releaseBranch = await automationContext.SpeckleClient Branch? releaseBranch = await automationContext.SpeckleClient
.BranchGet(automationContext.AutomationRunData.ProjectId, releaseBranchName, 1) .BranchGet(automationContext.AutomationRunData.ProjectId, releaseBranchName, 1)
.ConfigureAwait(false); .ConfigureAwait(false);
@@ -32,235 +33,240 @@ static class AutomateFunction
throw new Exception("Release branch was null"); throw new Exception("Release branch was null");
} }
// get the release branch latest commit
Commit releaseCommit = releaseBranch.commits.items.First(); Commit releaseCommit = releaseBranch.commits.items.First();
if (releaseCommit is null) if (releaseCommit is null)
{ {
throw new Exception("Release branch has no commits"); throw new Exception("Release branch has no commits");
} }
var tolerance = functionInputs.Tolerance; // get the test and release commit base
Console.WriteLine($"Comparing {testBranchName} against {releaseBranchName}");
// get the test and release commits
Console.WriteLine("Receiving test version"); Console.WriteLine("Receiving test version");
Base? testingCommitObject = await automationContext.ReceiveVersion(); Base testingCommitObject = await automationContext.ReceiveVersion();
Console.WriteLine("Received test version: " + testingCommitObject); Console.WriteLine("Received test version: " + testingCommitObject);
Console.WriteLine("Receiving release version"); Console.WriteLine("Receiving release version");
ServerTransport serverTransport = new ServerTransport( Base releaseCommitObject = await Utils.RecieveVersionAsync(
automationContext.SpeckleClient.Account, releaseCommit.id,
automationContext.AutomationRunData.ProjectId automationContext
); );
Base? releaseCommitObject = await Operations
.Receive(
(
await automationContext.SpeckleClient
.CommitGet(automationContext.AutomationRunData.ProjectId, releaseCommit.id)
.ConfigureAwait(continueOnCapturedContext: false)
).referencedObject,
serverTransport,
new MemoryTransport()
)
.ConfigureAwait(continueOnCapturedContext: false);
if (releaseCommitObject == null)
{
throw new Exception("Commit root object was null");
}
Console.WriteLine("Received release version: " + releaseCommitObject); Console.WriteLine("Received release version: " + releaseCommitObject);
// flatten both commits // Create dictionaries by appId (or speckle id if no appId exists) for the release and testing commits
IEnumerable<Base> releaseCommitObjects = releaseCommitObject.Flatten(); // note that it is possible for multiple objects to have the same app id
IEnumerable<Base> testCommitObjects = testingCommitObject.Flatten(); // (eg mapped schema objects have the same id as their parent geometry)
var releaseCommitObjectsDict = new Dictionary<string, Base>(); // and it is also possible for multiple objects (with no app id) to have the same speckle id
foreach (var releaseObject in releaseCommitObjects) // (eg the same mesh sent from gh multiple times)
Dictionary<string, List<Base>> releaseCommitAppIdDict = new();
Dictionary<string, List<Base>> releaseCommitSpeckleIdDict = new();
Dictionary<string, List<Base>> testingCommitAppIdDict = new();
Dictionary<string, List<Base>> testingCommitSpeckleIdDict = new();
Utils.CreateDictionaryFromBaseById(
releaseCommitObject,
out releaseCommitAppIdDict,
out releaseCommitSpeckleIdDict,
out int releaseObjectCount
);
Console.WriteLine(
$"Found {releaseObjectCount} objects in RELEASE with {releaseCommitAppIdDict.Count} unique applicationIds and {releaseCommitSpeckleIdDict} unique speckle ids (for objects with no application id)."
);
Utils.CreateDictionaryFromBaseById(
testingCommitObject,
out testingCommitAppIdDict,
out testingCommitSpeckleIdDict,
out int testingObjectCount
);
Console.WriteLine(
$"Found {testingObjectCount} objects in TESTING with {testingCommitAppIdDict.Count} unique applicationIds and {testingCommitSpeckleIdDict} unique speckle ids (for objects with no application id)."
);
// COMPARE COMMIT OBJECTS WITH APPLICATION IDS
// and store in hash lists where each object is (id, appId, type), and for modified, and additional string of property changes.
HashSet<Tuple<string, string?, string>> deletedAppIdObjects = new();
HashSet<Tuple<string, string?, string>> unchangedAppIdObjects = new();
HashSet<Tuple<string, string?, string>> addedAppIdObjects = new();
HashSet<Tuple<string, string?, string, string>> modifiedAppIdObjects = new();
// first find deleted objects in the testing commit and remove their keys from the release commit dict
foreach (string releaseAppId in releaseCommitAppIdDict.Keys)
{ {
if ( if (!testingCommitAppIdDict.ContainsKey(releaseAppId))
releaseObject.applicationId != null
&& !releaseCommitObjectsDict.ContainsKey(releaseObject.applicationId)
)
{ {
releaseCommitObjectsDict.Add(releaseObject.applicationId, releaseObject); releaseCommitAppIdDict[releaseAppId].ForEach(
o =>
deletedAppIdObjects.Add(
new Tuple<string, string?, string>(o.id, releaseAppId, o.speckle_type)
)
);
releaseCommitAppIdDict.Remove(releaseAppId);
} }
} }
Console.WriteLine( // then find unchanged, added, and modified objects by iterating through testing commit app ids
$"Found {releaseCommitObjects.Count()} objects in release version" foreach (string testingAppId in testingCommitAppIdDict.Keys)
);
Console.WriteLine($"Found {testCommitObjects.Count()} objects in release version");
// compare objects
int unchangedCount = 0;
var addedList = new List<Tuple<string, string>>();
var modifiedList = new List<Tuple<string, string, string>>();
foreach (Base testObject in testCommitObjects)
{ {
if ( List<Base> testObjects = testingCommitAppIdDict[testingAppId];
testObject.applicationId != null
&& releaseCommitObjectsDict.ContainsKey(testObject.applicationId) // test for added objects
) if (!releaseCommitAppIdDict.ContainsKey(testingAppId))
{ {
Base releaseObject = releaseCommitObjectsDict[testObject.applicationId]; testObjects.ForEach(
o =>
addedAppIdObjects.Add(
new Tuple<string, string?, string>(o.id, testingAppId, o.speckle_type)
)
);
}
else
{
List<Base> releaseObjects = releaseCommitAppIdDict[testingAppId];
// if these have the same hash, no properties have changed // test for unchanged objects
if (testObject.id == releaseObject.id) // by filtering the testing and release objects with matching speckle ids
Utils
.FilterListsBySpeckleIdMatch(testObjects, releaseObjects)
.ForEach(o => unchangedAppIdObjects.Add(o));
// for remaining objects, determine deleted objects
// and then compare them in order (assume modified) and handle leftovers (added)
// this is imperfect, as there's a chance we are not comparing the correct objects.
if (releaseObjects.Count > testObjects.Count)
{ {
unchangedCount++; for (int i = releaseObjects.Count - 1; i >= testObjects.Count; i--)
{
deletedAppIdObjects.Add(
new Tuple<string, string?, string>(
releaseObjects[i].id,
testingAppId,
releaseObjects[i].speckle_type
)
);
releaseObjects.RemoveAt(i);
}
} }
// if ids are different, find property differences
else for (int i = 0; i < testObjects.Count; i++)
{ {
var diffDictionary = new Dictionary<string, string>(); Base testObject = testObjects[i];
Dictionary<string, object?> releaseObjectPropDict =
releaseObject.GetMembers();
Dictionary<string, object?> testObjectPropDict = testObject.GetMembers();
foreach (var entry in testObjectPropDict)
{
if (releaseObjectPropDict.ContainsKey(entry.Key))
{
try
{
bool changed = !Equals(entry.Value, releaseObjectPropDict[entry.Key]);
if (changed)
{
string diff =
$"Property ({entry.Key}) changed from ({releaseObjectPropDict[entry.Key]}) to ({entry.Value})";
if (!diffDictionary.ContainsKey(entry.Key))
{
diffDictionary.Add(entry.Key, diff);
}
}
}
catch { }
releaseObjectPropDict.Remove(entry.Key);
}
else
{
if (!diffDictionary.ContainsKey(entry.Key))
{
diffDictionary.Add(entry.Key, ADDED);
}
}
}
// check if there are any props left on the release object - these were missing in the test object if (i < releaseObjectCount)
foreach (var entry in releaseObjectPropDict)
{ {
if (!diffDictionary.ContainsKey(entry.Key)) Base releaseObject = releaseObjects[i];
{
diffDictionary.Add(entry.Key, DELETED);
}
}
// add the diff dict info to the object // compare object properties to determine changes
if (diffDictionary.Count > 0) List<string> addedProps = new();
{ List<string> deletedProps = new();
List<Tuple<string, string>> modifiedProps = new();
Utils.CompareBaseProperties(
testObject,
releaseObject,
out addedProps,
out deletedProps,
out modifiedProps
);
var sb = new System.Text.StringBuilder(); var sb = new System.Text.StringBuilder();
foreach (var entry in diffDictionary) addedProps.ForEach(s => sb.AppendLine($"{ADDED} prop ({s})."));
{ deletedProps.ForEach(s => sb.AppendLine($"{DELETED} prop ({s})."));
sb.AppendLine($"{entry.Key}: {entry.Value}. "); modifiedProps.ForEach(
} t => sb.AppendLine($"{MODIFIED} ({t.Item2}) of prop ({t.Item1})")
);
modifiedList.Add( modifiedAppIdObjects.Add(
new Tuple<string, string, string>( new Tuple<string, string?, string, string>(
testObject.id, testObject.id,
testObject.applicationId,
testObject.speckle_type, testObject.speckle_type,
sb.ToString() sb.ToString()
) )
); );
//automationContext.AttachWarningToObjects( MODIFIED, new List<string>() { testObject.id }, sb.ToString()); }
// remaining test objects are considered added
else
{
addedAppIdObjects.Add(
new Tuple<string, string?, string>(
testObject.id,
testingAppId,
testObject.speckle_type
)
);
} }
} }
releaseCommitObjectsDict.Remove(testObject.applicationId);
}
else
{
// we're skipping objects without an applicationId for now, since we're doing so in the release commit
if (!string.IsNullOrEmpty(testObject.applicationId))
{
//automationContext.AttachInfoToObjects(ADDED, new List<string>() { testObject.id });
addedList.Add(
new Tuple<string, string>(testObject.id, testObject.speckle_type)
);
}
} }
} }
// if there are any remaining release commit objects, this indicates missing objects in the test run. // COMPARE COMMIT OBJECTS WITHOUT APPLICATION IDS USING SPECKLE IDS
var deletedList = new List<Tuple<string, string>>(); // since we only have 1 parameter of comparison, we can rule out any matching speckle ids as unchanged.
foreach (var entry in releaseCommitObjectsDict) // for all other objects, mark as modified, and indicate quantity of any added or deleted objects
{ // store modified in hash lists of (id, type)
deletedList.Add(new Tuple<string, string>(entry.Key, entry.Value.speckle_type)); HashSet<Tuple<string, string?, string>> unchangedSpeckleIdObjects = new();
} HashSet<Tuple<string, string>> changedSpeckleIdObjects = new();
// mark run failed if there are any added, modified, or deleted objects and report // first filter out matching speckle ids
if (addedList.Count + deletedList.Count + modifiedList.Count > 0) List<Base> flattenedTestingSpeckleIdDict = testingCommitSpeckleIdDict.Values
{ .SelectMany(o => o)
automationContext.MarkRunFailed( .ToList();
$"Run failed due to {addedList.Count} ADDED, {modifiedList.Count} MODIFIED, and {deletedList.Count} DELETED objects compared to the release commit." List<Base> flattenedReleaseSpeckleIdDict = releaseCommitSpeckleIdDict.Values
); .SelectMany(o => o)
.ToList();
Utils
.FilterListsBySpeckleIdMatch(
flattenedTestingSpeckleIdDict,
flattenedReleaseSpeckleIdDict
)
.ForEach(o => unchangedSpeckleIdObjects.Add(o));
addedList.ForEach( // then store all remaining testing objects as changed
o => Console.WriteLine($"ADDED object: id( {o.Item1} ), type( {o.Item2} )") flattenedTestingSpeckleIdDict.ForEach(
); o => changedSpeckleIdObjects.Add(new Tuple<string, string>(o.id, o.speckle_type))
deletedList.ForEach( );
o => Console.WriteLine($"DELETED object: id( {o.Item1} ), type( {o.Item2} )")
); // calculate count difference
modifiedList.ForEach( int speckleIdObjectCountDifference =
o => flattenedTestingSpeckleIdDict.Count - flattenedReleaseSpeckleIdDict.Count;
Console.WriteLine(
$"MODIFIED object: id( {o.Item1} ), type( {o.Item2} ), changed props:( {o.Item3} )" // REPORT ALL DIFF RESULTS FOR APP IDS AND SPECKLE IDS
) // mark run succeeded if there are no added, modified, or deleted app id objects, and no changed speckle id objects
); // mark run failed otherwise
if (
addedAppIdObjects.Count
+ deletedAppIdObjects.Count
+ modifiedAppIdObjects.Count
+ changedSpeckleIdObjects.Count
== 0
)
{
automationContext.MarkRunSuccess($"Run passed with no changes to objects.");
} }
else else
{ {
automationContext.MarkRunSuccess( automationContext.MarkRunFailed(
$"Run passed with {unchangedCount} unchanged objects." $"Run failed due to: {addedAppIdObjects.Count} {ADDED}, {modifiedAppIdObjects.Count} {MODIFIED}, and {deletedAppIdObjects.Count} {DELETED} objects WITH APP IDS, and {(speckleIdObjectCountDifference > 0 ? $"{speckleIdObjectCountDifference} {ADDED}" : $"{Math.Abs(speckleIdObjectCountDifference)} {DELETED}")} and {changedSpeckleIdObjects.Count} CHANGED objects WITHOUT APP IDS compared to the release commit. "
); );
}
}
public static bool Equals<T>(T a, T b) foreach (var added in addedAppIdObjects)
{ {
switch (a) Console.WriteLine(
{ $"{ADDED} {added.Item3} object: id( {added.Item1} ), appId: {added.Item2}"
case Base o: );
return b is Base bBase ? o.id == bBase.id : false; }
case List<object> aList:
if (b is List<object> bList && aList.Count == bList.Count) foreach (var deleted in deletedAppIdObjects)
{ {
for (int i = 0; i < aList.Count; i++) Console.WriteLine(
{ $"{DELETED} {deleted.Item3} object: id( {deleted.Item1} ), appId: {deleted.Item2}"
if (!Equals(aList[i], bList[i])) );
{ }
return false;
} foreach (var modified in modifiedAppIdObjects)
} {
return true; Console.WriteLine(
} $"{MODIFIED} {modified.Item3} object: id( {modified.Item1} ), appId: {modified.Item2}. Changed props: {modified.Item4}"
return false; );
case Dictionary<string, object> aDictionary: }
if (
b is Dictionary<string, object> bDictionary foreach (var changed in changedSpeckleIdObjects)
&& aDictionary.Count == bDictionary.Count {
) Console.WriteLine($"CHANGED {changed.Item2} object: id( {changed.Item1} )");
{ }
foreach (var entry in aDictionary)
{
if (
!bDictionary.ContainsKey(entry.Key)
|| !Equals(entry.Value, bDictionary[entry.Key])
)
{
return false;
}
}
return true;
}
return false;
default:
return EqualityComparer<T>.Default.Equals(a, b);
} }
} }
} }
@@ -7,7 +7,7 @@ using System.ComponentModel.DataAnnotations;
/// are valid and match the required schema. /// are valid and match the required schema.
struct FunctionInputs struct FunctionInputs
{ {
[Required] //[Required]
public double Tolerance; public double Tolerance;
//public string Exclusions; //public string Exclusions;
+275
View File
@@ -0,0 +1,275 @@
using Speckle.Automate.Sdk;
using Speckle.Automate.Sdk.Schema;
using Speckle.Core.Api;
using Speckle.Core.Models;
using Speckle.Core.Models.Extensions;
using Speckle.Core.Transports;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpeckleAutomateDotnetExample
{
internal class Utils
{
/// <summary>
/// Receives a commit object from the same project and account as the <paramref name="context"/>
/// </summary>
/// <param name="commitId"> The id of the commit to receive</param>
/// <param name="context">The Automation context</param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public static async Task<Base> RecieveVersionAsync(
string commitId,
AutomationContext context
)
{
ServerTransport serverTransport = new ServerTransport(
context.SpeckleClient.Account,
context.AutomationRunData.ProjectId
);
Base? receivedCommitObject = await Operations
.Receive(
(
await context.SpeckleClient
.CommitGet(context.AutomationRunData.ProjectId, commitId)
.ConfigureAwait(continueOnCapturedContext: false)
).referencedObject,
serverTransport,
new MemoryTransport()
)
.ConfigureAwait(continueOnCapturedContext: false);
if (receivedCommitObject == null)
{
throw new Exception("Commit root object was null");
}
return receivedCommitObject;
}
/// <summary>
/// Creates Dictionaries from a commit Base, using applicationId if available or speckle id if no applicationId exists.
/// </summary>
/// <param name="commit">The commit Base to create dictionaries from</param>
/// <param name="appIdDict">The dictionary of commit objects sorted by applicationId</param>
/// <param name="speckleIdDict">The dictionary of commit objects without applicationIds, sorted by speckle id</param>
public static void CreateDictionaryFromBaseById(
Base commit,
out Dictionary<string, List<Base>> appIdDict,
out Dictionary<string, List<Base>> speckleIdDict,
out int objectCount
)
{
IEnumerable<Base> commitObjects = commit.Flatten();
objectCount = commitObjects.Count();
appIdDict = new Dictionary<string, List<Base>>();
speckleIdDict = new Dictionary<string, List<Base>>();
foreach (var commitObject in commitObjects)
{
if (!string.IsNullOrWhiteSpace(commitObject.applicationId))
{
if (appIdDict.ContainsKey(commitObject.applicationId))
{
appIdDict[commitObject.applicationId].Add(commitObject);
}
else
{
appIdDict.Add(
commitObject.applicationId,
new List<Base>() { commitObject }
);
}
}
else
{
if (speckleIdDict.ContainsKey(commitObject.id))
{
speckleIdDict[commitObject.id].Add(commitObject);
}
else
{
speckleIdDict.Add(commitObject.id, new List<Base>() { commitObject });
}
}
}
}
/// <summary>
/// Filters two lists by removing all objects from <paramref name="setA"/> with a matching speckle id in <paramref name="setB"/>
/// </summary>
/// <param name="setA">The list to filter</param>
/// <param name="setB">The list to filter against</param>
/// <returns>A list of matches found in <paramref name="setA"/></returns>
public static List<Tuple<string, string?, string>> FilterListsBySpeckleIdMatch(
List<Base> setA,
List<Base> setB
)
{
var matches = new List<Tuple<string, string?, string>>();
for (int i = setA.Count - 1; i >= 0; i--)
{
var testObject = setA[i];
for (int j = setB.Count - 1; j >= 0; j--)
{
var releaseObject = setB[j];
// if a match was found, remove from both lists and add to matches
if (testObject.id == releaseObject.id)
{
matches.Add(
new Tuple<string, string?, string>(
testObject.id,
testObject.applicationId,
testObject.speckle_type
)
);
setA.RemoveAt(i);
setB.RemoveAt(j);
}
}
}
return matches;
}
/// <summary>
/// Compares the properties of <paramref name="a"/> against <paramref name="b"/>
/// </summary>
/// <param name="a"></param>
/// <param name="b"></param>
/// <param name="addedProps">Properties on <paramref name="a"/> that do not exist on <paramref name="b"/></param>
/// <param name="deletedProps">Properties on <paramref name="b"/> that do not exist on <paramref name="a"/></param>
/// <param name="modifiedProps">Properties on <paramref name="a"/> that have a different value on <paramref name="b"/>. The second string is the category of modification (eg count/primitive/base)</param>
public static void CompareBaseProperties(
Base a,
Base b,
out List<string> addedProps,
out List<string> deletedProps,
out List<Tuple<string, string>> modifiedProps
)
{
addedProps = new List<string>();
deletedProps = new List<string>();
modifiedProps = new List<Tuple<string, string>>();
Dictionary<string, object?> bObjectPropDict = b.GetMembers();
Dictionary<string, object?> aObjectPropDict = a.GetMembers();
foreach (KeyValuePair<string, object?> entry in aObjectPropDict)
{
if (bObjectPropDict.ContainsKey(entry.Key))
{
bool changed = !IsEqual(
entry.Value,
bObjectPropDict[entry.Key],
out string category
);
if (changed)
{
modifiedProps.Add(new Tuple<string, string>(entry.Key, category));
}
bObjectPropDict.Remove(entry.Key);
}
else
{
addedProps.Add(entry.Key);
}
}
// check if there are any props left on the b - these were missing in the a
foreach (var entry in bObjectPropDict)
{
deletedProps.Add(entry.Key);
}
}
/// <summary>
/// Determines equality between two objects
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="a"></param>
/// <param name="b"></param>
/// <param name="category">The category of the modification, if objects are not equal</param>
/// <returns></returns>
public static bool IsEqual<T>(T a, T b, out string category)
{
category = "Value";
switch (a)
{
case Base aBase:
if (b is Base bBase && bBase.speckle_type == aBase.speckle_type)
{
return aBase.id == bBase.id;
}
else
{
category = "Type";
return false;
}
case List<object> aList:
if (b is List<object> bList)
{
if (aList.Count != bList.Count)
{
category = "Count";
return false;
}
for (int i = 0; i < aList.Count; i++)
{
if (!Equals(aList[i], bList[i]))
{
return false;
}
}
return true;
}
else
{
category = "Type";
return false;
}
case Dictionary<string, object> aDictionary:
if (b is Dictionary<string, object> bDictionary)
{
if (aDictionary.Count != bDictionary.Count)
{
category = "Count";
return false;
}
foreach (var entry in aDictionary)
{
if (
!bDictionary.ContainsKey(entry.Key)
|| !Equals(entry.Value, bDictionary[entry.Key])
)
{
return false;
}
}
return true;
}
else
{
category = "Type";
return false;
}
default:
try
{
return EqualityComparer<T>.Default.Equals(a, b);
}
catch
{
return false;
}
}
}
}
}