diff --git a/FakeXrmEasy.2013/Properties/AssemblyInfo.cs b/FakeXrmEasy.2013/Properties/AssemblyInfo.cs index 3760e94c..28460eb6 100644 --- a/FakeXrmEasy.2013/Properties/AssemblyInfo.cs +++ b/FakeXrmEasy.2013/Properties/AssemblyInfo.cs @@ -32,5 +32,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.8.1")] -[assembly: AssemblyFileVersion("1.8.1")] +[assembly: AssemblyVersion("1.8.2")] +[assembly: AssemblyFileVersion("1.8.2")] diff --git a/FakeXrmEasy.2015/FakeXrmEasy.2015.csproj b/FakeXrmEasy.2015/FakeXrmEasy.2015.csproj index 13569b80..df466a07 100644 --- a/FakeXrmEasy.2015/FakeXrmEasy.2015.csproj +++ b/FakeXrmEasy.2015/FakeXrmEasy.2015.csproj @@ -18,7 +18,7 @@ full false bin\Debug\ - DEBUG;TRACE + TRACE;DEBUG;FAKE_XRM_EASY_2015 prompt 4 false diff --git a/FakeXrmEasy.2015/Properties/AssemblyInfo.cs b/FakeXrmEasy.2015/Properties/AssemblyInfo.cs index 27b0bd24..382ec22a 100644 --- a/FakeXrmEasy.2015/Properties/AssemblyInfo.cs +++ b/FakeXrmEasy.2015/Properties/AssemblyInfo.cs @@ -32,5 +32,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.8.1.0")] -[assembly: AssemblyFileVersion("1.8.1.0")] +[assembly: AssemblyVersion("1.8.2.0")] +[assembly: AssemblyFileVersion("1.8.2.0")] diff --git a/FakeXrmEasy.Tests.2013/buildNugetPackages.bat b/FakeXrmEasy.Tests.2013/buildNugetPackages.bat index ce106396..b53af215 100644 --- a/FakeXrmEasy.Tests.2013/buildNugetPackages.bat +++ b/FakeXrmEasy.Tests.2013/buildNugetPackages.bat @@ -1,5 +1,5 @@ copy ..\FakeXrmEasy.2013\bin\Debug\FakeXrmEasy.dll .\build\lib\net40 cd build nuget pack FakeXrmEasy.2013.dll.nuspec -nuget push FakeXrmEasy.2013.1.8.1.nupkg +nuget push FakeXrmEasy.2013.1.8.2.nupkg pause \ No newline at end of file diff --git a/FakeXrmEasy.Tests.2015/FakeXrmEasy.Tests.2015.csproj b/FakeXrmEasy.Tests.2015/FakeXrmEasy.Tests.2015.csproj index 2fdabdaf..2a10fddf 100644 --- a/FakeXrmEasy.Tests.2015/FakeXrmEasy.Tests.2015.csproj +++ b/FakeXrmEasy.Tests.2015/FakeXrmEasy.Tests.2015.csproj @@ -23,7 +23,7 @@ full false bin\Debug\ - DEBUG;TRACE + TRACE;DEBUG;FAKE_XRM_EASY_2015 prompt 4 diff --git a/FakeXrmEasy.Tests.2015/build/FakeXrmEasy.2015.dll.nuspec b/FakeXrmEasy.Tests.2015/build/FakeXrmEasy.2015.dll.nuspec index 748bc832..0ed98dc9 100644 --- a/FakeXrmEasy.Tests.2015/build/FakeXrmEasy.2015.dll.nuspec +++ b/FakeXrmEasy.Tests.2015/build/FakeXrmEasy.2015.dll.nuspec @@ -2,7 +2,7 @@ FakeXrmEasy.2015 - 1.8.1 + 1.8.2 @jordimontana @jordimontana https://raw.githubusercontent.com/jordimontana82/fake-xrm-easy/master/LICENSE.md @@ -10,7 +10,7 @@ true Utilities to streamline unit testing in Dynamics CRM 2013 by faking the IOrganizationService using FakeItEasy and make it work against an In-Memory context. - fixed Assembly Name + Fixed issues with joins / linked entities and filters across versions as a new EntityName property was introduced in ConditionExpression >= 2013 Copyright 2015 xrm dynamics crm 2013 unit testing mock mocking fake fakes diff --git a/FakeXrmEasy.Tests.2015/buildNugetPackages.bat b/FakeXrmEasy.Tests.2015/buildNugetPackages.bat index fda375a5..22288a06 100644 --- a/FakeXrmEasy.Tests.2015/buildNugetPackages.bat +++ b/FakeXrmEasy.Tests.2015/buildNugetPackages.bat @@ -1,5 +1,5 @@ copy ..\FakeXrmEasy.2015\bin\Debug\FakeXrmEasy.dll .\build\lib\net452 cd build nuget pack FakeXrmEasy.2015.dll.nuspec -nuget push FakeXrmEasy.2015.1.8.1.nupkg +nuget push FakeXrmEasy.2015.1.8.2.nupkg pause \ No newline at end of file diff --git a/FakeXrmEasy.Tests/FakeContextTests/LinqTests/EqualityWithDifferentDataTypesTests.cs b/FakeXrmEasy.Tests/FakeContextTests/LinqTests/EqualityWithDifferentDataTypesTests.cs index 1aec5a59..02aab5b4 100644 --- a/FakeXrmEasy.Tests/FakeContextTests/LinqTests/EqualityWithDifferentDataTypesTests.cs +++ b/FakeXrmEasy.Tests/FakeContextTests/LinqTests/EqualityWithDifferentDataTypesTests.cs @@ -339,8 +339,9 @@ public void When_executing_a_linq_query_with_equals_between_2_activityparties_re using (XrmServiceContext ctx = new XrmServiceContext(service)) { var activities = (from pointer in ctx.CreateQuery() - join party in ctx.CreateQuery() on pointer.Id equals party.ActivityId.Id - where party.PartyId.Id == contactId + join party in ctx.CreateQuery() on pointer.ActivityId.Value equals party.ActivityId.Id + // from party in ctx.CreateQuery() //on pointer.ActivityId.Value equals party.ActivityId.Id + where party.PartyId.Id == contactId select pointer).ToList(); Assert.True(activities.Count == 1); diff --git a/FakeXrmEasy.Tests/FakeContextTests/LinqTests/FakeContextTestLinqQueries.cs b/FakeXrmEasy.Tests/FakeContextTests/LinqTests/FakeContextTestLinqQueries.cs index 29fb7af6..99afdcad 100644 --- a/FakeXrmEasy.Tests/FakeContextTests/LinqTests/FakeContextTestLinqQueries.cs +++ b/FakeXrmEasy.Tests/FakeContextTests/LinqTests/FakeContextTestLinqQueries.cs @@ -833,5 +833,39 @@ public void When_doing_a_crm_linq_query_that_produces_a_filter_expression_plus_c Assert.True(matches.Count == 2); } } + + [Fact(DisplayName = "When_doing_a_join_with_filter_then_can_filter_by_the_joined_entity_attributes")] + public void When_doing_a_join_with_filter_then_can_filter_by_the_joined_entity_attributes() + { + //REVIEW: Different implementations of the ConditionExpression class in Microsoft.Xrm.Sdk (which has EntityName property for versions >= 2013) + + var fakedContext = new XrmFakedContext(); + fakedContext.ProxyTypesAssembly = Assembly.GetExecutingAssembly(); + + var contactId = Guid.NewGuid(); + var accountId = Guid.NewGuid(); + var accountId2 = Guid.NewGuid(); + + //Contact is related to first account, but because first account is not related to itself then the query must return 0 records + fakedContext.Initialize(new List() { + new Account() { Id = accountId, Name="Account1" }, + new Account() { Id = accountId2, Name = "Account2" }, + new Contact() { Id = contactId, ParentCustomerId = new EntityReference(Account.EntityLogicalName, accountId), + NumberOfChildren = 2, FirstName = "Contact" }, + new Contact() {Id = Guid.NewGuid(), ParentCustomerId = new EntityReference(Account.EntityLogicalName, accountId2) } + }); + + var service = fakedContext.GetFakedOrganizationService(); + + using (XrmServiceContext ctx = new XrmServiceContext(service)) + { + var matches = (from c in ctx.CreateQuery() + join account in ctx.CreateQuery() on c.ParentCustomerId.Id equals account.AccountId + where account.Name == "Account1" + select c).ToList(); + + Assert.True(matches.Count == 1); + } + } } } diff --git a/FakeXrmEasy.Tests/FakeContextTests/TranslateQueryExpressionTests/FakeContextTestTranslateQueryExpression.cs b/FakeXrmEasy.Tests/FakeContextTests/TranslateQueryExpressionTests/FakeContextTestTranslateQueryExpression.cs index 3779fb7d..f67f831e 100644 --- a/FakeXrmEasy.Tests/FakeContextTests/TranslateQueryExpressionTests/FakeContextTestTranslateQueryExpression.cs +++ b/FakeXrmEasy.Tests/FakeContextTests/TranslateQueryExpressionTests/FakeContextTestTranslateQueryExpression.cs @@ -310,8 +310,11 @@ public void When_executing_a_query_expression_with_all_attributes_all_of_them_ar var firstContact = result.FirstOrDefault(); var lastContact = result.LastOrDefault(); - Assert.True(firstContact.Attributes.Count == 3 + 4); //Contact 1 (the extra four are the CreatedOn, ModifiedOn, CreatedBy, ModifiedBy attributes generated automatically - Assert.True(lastContact.Attributes.Count == 3 + 4); //Contact 2 + //Contact 1 attributes = 3 + 4 (the extra four are the CreatedOn, ModifiedOn, CreatedBy, ModifiedBy attributes generated automatically + //+ Attributes from the join(account) = 1 + 4 (the extra four are the CreatedOn, ModifiedOn, CreatedBy, ModifiedBy attributes generated automatically + + Assert.True(firstContact.Attributes.Count == 3 + 1 + 4 * 2); + Assert.True(lastContact.Attributes.Count == 3 + 1 + 4 * 2); //Contact 2 } [Fact] @@ -342,6 +345,7 @@ public void When_executing_a_query_expression_without_columnset_no_attributes_ar } ); + qe.ColumnSet = new ColumnSet(false); var result = XrmFakedContext.TranslateQueryExpressionToLinq(context, qe); Assert.True(result.Count() == 2); diff --git a/FakeXrmEasy.Tests/build/FakeXrmEasy.dll.nuspec b/FakeXrmEasy.Tests/build/FakeXrmEasy.dll.nuspec index e8d85e0f..9ffe52d2 100644 --- a/FakeXrmEasy.Tests/build/FakeXrmEasy.dll.nuspec +++ b/FakeXrmEasy.Tests/build/FakeXrmEasy.dll.nuspec @@ -2,7 +2,7 @@ FakeXrmEasy - 1.8.1 + 1.8.2 @jordimontana @jordimontana https://raw.githubusercontent.com/jordimontana82/fake-xrm-easy/master/LICENSE.md @@ -10,8 +10,7 @@ true Utilities to streamline unit testing in Dynamics CRM by faking the IOrganizationService using FakeItEasy and make it work against an In-Memory context. - Added the ability to pass custom mocks to the context. - Bug fix. + Fixed issues with joins / linked entities and filters across versions as a new EntityName property was introduced in ConditionExpression >= 2013 Copyright 2015 xrm dynamics crm unit testing mock mocking fake fakes diff --git a/FakeXrmEasy.Tests/build/lib/net40/FakeXrmEasy.dll b/FakeXrmEasy.Tests/build/lib/net40/FakeXrmEasy.dll index aa58a6b5..0c40105a 100644 Binary files a/FakeXrmEasy.Tests/build/lib/net40/FakeXrmEasy.dll and b/FakeXrmEasy.Tests/build/lib/net40/FakeXrmEasy.dll differ diff --git a/FakeXrmEasy.Tests/buildNugetPackages.bat b/FakeXrmEasy.Tests/buildNugetPackages.bat index 7d019175..a9024baf 100644 --- a/FakeXrmEasy.Tests/buildNugetPackages.bat +++ b/FakeXrmEasy.Tests/buildNugetPackages.bat @@ -1,5 +1,5 @@ copy ..\FakeXrmEasy\bin\Debug\FakeXrmEasy.dll .\build\lib\net40 cd build nuget pack FakeXrmEasy.dll.nuspec -nuget push FakeXrmEasy.1.8.1.nupkg +nuget push FakeXrmEasy.1.8.2.nupkg pause \ No newline at end of file diff --git a/FakeXrmEasy/Extensions/EntityExtensions.cs b/FakeXrmEasy/Extensions/EntityExtensions.cs index f125fdbc..4f82eaee 100644 --- a/FakeXrmEasy/Extensions/EntityExtensions.cs +++ b/FakeXrmEasy/Extensions/EntityExtensions.cs @@ -35,9 +35,14 @@ public static Entity AddAttribute(this Entity e, string key, object value) /// public static Entity ProjectAttributes(this Entity e, ColumnSet columnSet, XrmFakedContext context) { - if (columnSet == null) return e; + return ProjectAttributes(e, new QueryExpression() { ColumnSet = columnSet }, context); + } - if (columnSet.AllColumns) + public static Entity ProjectAttributes(this Entity e, QueryExpression qe, XrmFakedContext context) + { + if (qe.ColumnSet == null) return e; + + if (qe.ColumnSet.AllColumns) { return e; //return all the original attributes } @@ -62,7 +67,7 @@ public static Entity ProjectAttributes(this Entity e, ColumnSet columnSet, XrmFa else projected = new Entity(e.LogicalName) { Id = e.Id }; - foreach (var attKey in columnSet.Columns) + foreach (var attKey in qe.ColumnSet.Columns) { if (e.Attributes.ContainsKey(attKey)) projected[attKey] = e[attKey]; @@ -76,12 +81,22 @@ public static Entity ProjectAttributes(this Entity e, ColumnSet columnSet, XrmFa } } - //Plus the aliased attributes, if any - foreach (var attKey in e.Attributes.Keys) + //Plus attributes from joins + foreach(var le in qe.LinkEntities) { - if(e[attKey] is AliasedValue && !projected.Attributes.ContainsKey(attKey)) - projected[attKey] = e[attKey]; + foreach (var attKey in le.Columns.Columns) + { + var sAlias = string.IsNullOrWhiteSpace(le.EntityAlias) ? le.LinkToEntityName : le.EntityAlias; + var linkedAttKey = sAlias + "." + attKey; + if (e.Attributes.ContainsKey(linkedAttKey) || le.Columns.AllColumns) + projected[linkedAttKey] = e[linkedAttKey]; + } } + //foreach (var attKey in e.Attributes.Keys) + //{ + // if(e[attKey] is AliasedValue && !projected.Attributes.ContainsKey(attKey)) + // projected[attKey] = e[attKey]; + //} return projected; } } diff --git a/FakeXrmEasy/Properties/AssemblyInfo.cs b/FakeXrmEasy/Properties/AssemblyInfo.cs index afe97e70..6946b0cb 100644 --- a/FakeXrmEasy/Properties/AssemblyInfo.cs +++ b/FakeXrmEasy/Properties/AssemblyInfo.cs @@ -32,6 +32,6 @@ // // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: -[assembly: AssemblyVersion("1.8.1.0")] -[assembly: AssemblyFileVersion("1.8.1.0")] -[assembly: AssemblyInformationalVersion("1.8.1")] +[assembly: AssemblyVersion("1.8.2.0")] +[assembly: AssemblyFileVersion("1.8.2.0")] +[assembly: AssemblyInformationalVersion("1.8.2")] diff --git a/FakeXrmEasy/XrmFakedContext.Queries.cs b/FakeXrmEasy/XrmFakedContext.Queries.cs index b32e899e..cc57e323 100644 --- a/FakeXrmEasy/XrmFakedContext.Queries.cs +++ b/FakeXrmEasy/XrmFakedContext.Queries.cs @@ -117,6 +117,11 @@ public static IQueryable TranslateLinkedEntityToLinq(XrmFakedContext con var inner = context.CreateQuery(le.LinkToEntityName); + if(!le.Columns.AllColumns && le.Columns.Columns.Count == 0) + { + le.Columns.AllColumns = true; //Add all columns in the joined entity, otherwise we can't filter by related attributes, then the Select will actually choose which ones we need + } + switch (le.JoinOperator) { case JoinOperator.Inner: @@ -170,13 +175,12 @@ public static IQueryable TranslateQueryExpressionToLinq(XrmFakedContext // Compose the expression tree that represents the parameter to the predicate. ParameterExpression entity = Expression.Parameter(typeof(Entity)); - var expTreeBody = TranslateFilterExpressionToExpression(qe.Criteria, entity); + var expTreeBody = TranslateQueryExpressionFiltersToExpression(qe, entity); Expression> lambda = Expression.Lambda>(expTreeBody, entity); query = query.Where(lambda); //Project the attributes in the root column set (must be applied after the where clause, not before!!) - if (qe.ColumnSet != null && !qe.ColumnSet.AllColumns) - query = query.Select(x => x.ProjectAttributes(qe.ColumnSet, context)); + query = query.Select(x => x.ProjectAttributes(qe, context)); //Sort results if (qe.Orders != null) @@ -208,6 +212,30 @@ protected static Expression TranslateConditionExpression(ConditionExpression c, "Attributes" ); + + //If the attribute comes from a joined entity, then we need to access the attribute from the join + //But the entity name attribute only exists >= 2013 +#if FAKE_XRM_EASY_2013 || FAKE_XRM_EASY_2015 + string attributeName = ""; + + if (!string.IsNullOrWhiteSpace(c.EntityName)) + { + attributeName = c.EntityName + "." + c.AttributeName; + } + else + attributeName = c.AttributeName; + + Expression containsAttributeExpression = Expression.Call( + attributesProperty, + typeof(AttributeCollection).GetMethod("ContainsKey", new Type[] { typeof(string) }), + Expression.Constant(attributeName) + ); + + Expression getAttributeValueExpr = Expression.Property( + attributesProperty, "Item", + Expression.Constant(attributeName, typeof(string)) + ); +#else Expression containsAttributeExpression = Expression.Call( attributesProperty, typeof(AttributeCollection).GetMethod("ContainsKey", new Type[] { typeof(string) }), @@ -215,9 +243,11 @@ protected static Expression TranslateConditionExpression(ConditionExpression c, ); Expression getAttributeValueExpr = Expression.Property( - attributesProperty, "Item", - Expression.Constant(c.AttributeName, typeof(string)) - ); + attributesProperty, "Item", + Expression.Constant(c.AttributeName, typeof(string)) + ); +#endif + Expression getNonBasicValueExpr = getAttributeValueExpr; @@ -288,6 +318,23 @@ protected static Expression GetAppropiateTypedValue(object value) } protected static Expression GetAppropiateCastExpressionBasedOnValue(Expression input, object value) + { + var typedExpression = GetAppropiateCastExpressionBasedOnValueInherentType(input, value); + + //Now, any value (entity reference, string, int, etc,... could be wrapped in an AliasedValue object + //So let's add this + var getValueFromAliasedValueExp = Expression.Call(Expression.Convert(input, typeof(AliasedValue)), + typeof(AliasedValue).GetMethod("get_Value")); + + var exp = Expression.Condition(Expression.TypeIs(input, typeof(AliasedValue)), + GetAppropiateCastExpressionBasedOnValueInherentType(getValueFromAliasedValueExp, value), + typedExpression //Not an aliased value + ); + + return exp; + } + + protected static Expression GetAppropiateCastExpressionBasedOnValueInherentType(Expression input, object value) { if (value is Guid) return GetAppropiateCastExpressionBasedGuid(input); //Could be compared against an EntityReference @@ -297,42 +344,39 @@ protected static Expression GetAppropiateCastExpressionBasedOnValue(Expression i return GetAppropiateCastExpressionBasedOnDecimal(input); //Could be compared against a Money if (value is bool) return GetAppropiateCastExpressionBasedOnBoolean(input); //Could be a BooleanManagedProperty - - //Other basic types conversions - //Special case => datetime is sent as a string if (value is string) { - DateTime dtDateTimeConversion; - if (DateTime.TryParse(value.ToString(), out dtDateTimeConversion)) - { - return Expression.Convert(input, typeof(DateTime)); - } + return GetAppropiateCastExpressionBasedOnString(input, value); } - return Expression.Convert(input, value.GetType()); //Default type conversion + return GetAppropiateCastExpressionDefault(input, value); //any other type + } + protected static Expression GetAppropiateCastExpressionBasedOnString(Expression input, object value) + { + DateTime dtDateTimeConversion; + if (DateTime.TryParse(value.ToString(), out dtDateTimeConversion)) + { + return Expression.Convert(input, typeof(DateTime)); + } + return GetAppropiateCastExpressionDefault(input, value); //Non datetime string } + protected static Expression GetAppropiateCastExpressionDefault(Expression input, object value) + { + return Expression.Convert(input, value.GetType()); //Default type conversion + } protected static Expression GetAppropiateCastExpressionBasedGuid(Expression input) { + var getIdFromEntityReferenceExpr = Expression.Call(Expression.TypeAs(input, typeof(EntityReference)), + typeof(EntityReference).GetMethod("get_Id")); + return Expression.Condition( Expression.TypeIs(input, typeof(EntityReference)), //If input is an entity reference, compare the Guid against the Id property Expression.Convert( - Expression.Call(Expression.TypeAs(input, typeof(EntityReference)), - typeof(EntityReference).GetMethod("get_Id")), + getIdFromEntityReferenceExpr, typeof(Guid)), - Expression.Condition(Expression.AndAlso(Expression.TypeIs(input, typeof(AliasedValue)), //If input is an AliasedValue which has an EntityReference, compare against the Id value as well - Expression.TypeIs(Expression.Call(Expression.TypeAs(input, typeof(AliasedValue)), - typeof(AliasedValue).GetMethod("get_Value")), - typeof(EntityReference))), - Expression.Call(Expression.TypeAs(Expression.Convert( - Expression.Call(Expression.TypeAs(input, typeof(AliasedValue)), - typeof(AliasedValue).GetMethod("get_Value")), - typeof(Guid)) - , typeof(EntityReference)), - typeof(EntityReference).GetMethod("get_Id")), - - Expression.Condition(Expression.TypeIs(input, typeof(Guid)), //If any other case, then just compare it as a Guid directly + Expression.Condition(Expression.TypeIs(input, typeof(Guid)), //If any other case, then just compare it as a Guid directly Expression.Convert(input, typeof(Guid)), - Expression.Constant(Guid.Empty)))); + Expression.Constant(Guid.Empty, typeof(Guid)))); } @@ -525,6 +569,61 @@ protected static BinaryExpression TranslateMultipleFilterExpressions(List(); + foreach(var le in qe.LinkEntities) + { + var e = TranslateLinkedEntityFilterExpressionToExpression(le, entity); + linkedEntitiesQueryExpressions.Add(e); + } + + if(linkedEntitiesQueryExpressions.Count > 0 && qe.Criteria != null) + { + //Return the and of the two + Expression andExpression = Expression.Constant(true); + foreach(var e in linkedEntitiesQueryExpressions) + { + andExpression = Expression.And(e, andExpression); + + } + var feExpression = TranslateFilterExpressionToExpression(qe.Criteria, entity); + return Expression.And(andExpression, feExpression); + } + else if (linkedEntitiesQueryExpressions.Count > 0) + { + //Linked entity expressions only + Expression andExpression = Expression.Constant(true); + foreach (var e in linkedEntitiesQueryExpressions) + { + andExpression = Expression.And(e, andExpression); + + } + return andExpression; + } + else + { + //Criteria only + return TranslateFilterExpressionToExpression(qe.Criteria, entity); + } + } protected static Expression TranslateFilterExpressionToExpression(FilterExpression fe, ParameterExpression entity) { if (fe == null) return Expression.Constant(true);