/// <summary> /// Builds an expression for an attribute field /// </summary> /// <param name="serviceInstance">The service instance.</param> /// <param name="parameterExpression">The parameter expression.</param> /// <param name="entityField">The entity field.</param> /// <param name="values">The filter parameter values: FieldName, <see cref="ComparisonType">Comparison Type</see>, (optional) Comparison Value(s)</param> /// <returns></returns> public static Expression GetAttributeExpression(IService serviceInstance, ParameterExpression parameterExpression, EntityField entityField, List <string> values) { if (!values.Any()) { // if no filter parameter values where specified, don't filter return(new NoAttributeFilterExpression()); } var service = new AttributeValueService(( RockContext )serviceInstance.Context); var attributeValues = service.Queryable().Where(v => v.EntityId.HasValue); AttributeCache attributeCache = null; if (entityField.AttributeGuid.HasValue) { attributeCache = AttributeCache.Get(entityField.AttributeGuid.Value); var attributeId = attributeCache != null ? attributeCache.Id : 0; attributeValues = attributeValues.Where(v => v.AttributeId == attributeId); } else { attributeValues = attributeValues.Where(v => v.Attribute.Key == entityField.Name && v.Attribute.FieldTypeId == entityField.FieldType.Id); } ParameterExpression attributeValueParameterExpression = Expression.Parameter(typeof(AttributeValue), "v"); // Determine the appropriate comparison type to use for this Expression. // Attribute Value records only exist for Entities that have a value specified for the Attribute. // Therefore, if the specified comparison works by excluding certain values we must invert our filter logic: // first we find the Attribute Values that match those values and then we exclude the associated Entities from the result set. ComparisonType?comparisonType = ComparisonType.EqualTo; ComparisonType?evaluatedComparisonType = comparisonType; // If Values.Count >= 2, then Values[0] is ComparisonType, and Values[1] is a CompareToValue. Otherwise, Values[0] is a CompareToValue (for example, a SingleSelect attribute) if (values.Count >= 2) { comparisonType = values[0].ConvertToEnumOrNull <ComparisonType>(); switch (comparisonType) { case ComparisonType.DoesNotContain: evaluatedComparisonType = ComparisonType.Contains; break; case ComparisonType.IsBlank: evaluatedComparisonType = ComparisonType.IsNotBlank; break; case ComparisonType.LessThan: evaluatedComparisonType = ComparisonType.GreaterThanOrEqualTo; break; case ComparisonType.LessThanOrEqualTo: evaluatedComparisonType = ComparisonType.GreaterThan; break; case ComparisonType.NotEqualTo: evaluatedComparisonType = ComparisonType.EqualTo; break; default: evaluatedComparisonType = comparisonType; break; } values[0] = evaluatedComparisonType.ToString(); } var filterExpression = entityField.FieldType.Field.AttributeFilterExpression(entityField.FieldConfig, values, attributeValueParameterExpression); if (filterExpression != null) { if (filterExpression is NoAttributeFilterExpression) { // Special Case: If AttributeFilterExpression returns NoAttributeFilterExpression, just return the NoAttributeFilterExpression. // For example, If this is a CampusFieldType and they didn't pick any campus, we don't want to do any filtering for this datafilter. return(filterExpression); } else { attributeValues = attributeValues.Where(attributeValueParameterExpression, filterExpression, null); } } else { // AttributeFilterExpression returned NULL ( the FieldType didn't specify any additional filter on AttributeValue), // ideally the FieldType should have returned a NoAttributeFilterExpression, but just in case, don't filter System.Diagnostics.Debug.WriteLine($"Unexpected NULL result from FieldType.Field.AttributeFilterExpression for { entityField.FieldType }"); return(new NoAttributeFilterExpression()); } IQueryable <int> ids = attributeValues.Select(v => v.EntityId.Value); MemberExpression propertyExpression = Expression.Property(parameterExpression, "Id"); ConstantExpression idsExpression = Expression.Constant(ids.AsQueryable(), typeof(IQueryable <int>)); Expression expression = Expression.Call(typeof(Queryable), "Contains", new Type[] { typeof(int) }, idsExpression, propertyExpression); if (attributeCache != null) { // Test the default value against the expression filter. If it pass, then we can include all the attribute values with no value. var comparedToDefault = IsComparedToValue(attributeValueParameterExpression, filterExpression, attributeCache.DefaultValue); if (comparedToDefault) { var allAttributeValueIds = service.Queryable().Where(v => v.Attribute.Id == attributeCache.Id && v.EntityId.HasValue && !string.IsNullOrEmpty(v.Value)).Select(a => a.EntityId.Value); ConstantExpression allIdsExpression = Expression.Constant(allAttributeValueIds.AsQueryable(), typeof(IQueryable <int>)); Expression notContainsExpression = Expression.Not(Expression.Call(typeof(Queryable), "Contains", new Type[] { typeof(int) }, allIdsExpression, propertyExpression)); expression = Expression.Or(expression, notContainsExpression); } // If there is an EntityTypeQualifierColumn/Value on this attribute, also narrow down the entity query to the ones with matching QualifierColumn/Value if (attributeCache.EntityTypeQualifierColumn.IsNotNullOrWhiteSpace() && attributeCache.EntityTypeQualifierValue.IsNotNullOrWhiteSpace()) { Expression qualifierParameterExpression = null; PropertyInfo qualifierColumnProperty = parameterExpression.Type.GetProperty(attributeCache.EntityTypeQualifierColumn); // make sure the QualifierColumn is an actual mapped property on the Entity if (qualifierColumnProperty != null && qualifierColumnProperty.GetCustomAttribute <NotMappedAttribute>() == null) { qualifierParameterExpression = parameterExpression; } else { if (attributeCache.EntityTypeQualifierColumn == "GroupTypeId" && parameterExpression.Type == typeof(Rock.Model.GroupMember)) { // Special Case for GroupMember with Qualifier of 'GroupTypeId' (which is really Group.GroupTypeId) qualifierParameterExpression = Expression.Property(parameterExpression, "Group"); } else if (attributeCache.EntityTypeQualifierColumn == "RegistrationTemplateId" && parameterExpression.Type == typeof(Rock.Model.RegistrationRegistrant)) { // Special Case for RegistrationRegistrant with Qualifier of 'RegistrationTemplateId' (which is really Registration.RegistrationInstance.RegistrationTemplateId) qualifierParameterExpression = Expression.Property(parameterExpression, "Registration"); qualifierParameterExpression = Expression.Property(qualifierParameterExpression, "RegistrationInstance"); } else { // Unable to determine how the EntityTypeQualiferColumn relates to the Entity. Probably will be OK, but spit out a debug message System.Diagnostics.Debug.WriteLine($"Unable to determine how the EntityTypeQualiferColumn {attributeCache.EntityTypeQualifierColumn} relates to entity {parameterExpression.Type} on attribute {attributeCache.Name}:{attributeCache.Guid}"); } } if (qualifierParameterExpression != null) { // if we figured out the EntityQualifierColumn/Value expression, apply it // This would effectively add something like 'WHERE [GroupTypeId] = 10' to the WHERE clause MemberExpression entityQualiferColumnExpression = Expression.Property(qualifierParameterExpression, attributeCache.EntityTypeQualifierColumn); object entityTypeQualifierValueAsType = Convert.ChangeType(attributeCache.EntityTypeQualifierValue, entityQualiferColumnExpression.Type); Expression entityQualiferColumnEqualExpression = Expression.Equal(entityQualiferColumnExpression, Expression.Constant(entityTypeQualifierValueAsType, entityQualiferColumnExpression.Type)); // If the qualifier Column is GroupTypeId, we'll have to do an OR clause of all the GroupTypes that inherit from this // This would effectively add something like 'WHERE ([GroupTypeId] = 10) OR ([GroupTypeId] = 12) OR ([GroupTypeId] = 17)' to the WHERE clause if (attributeCache.EntityTypeQualifierColumn == "GroupTypeId" && attributeCache.EntityTypeQualifierValue.AsIntegerOrNull().HasValue) { var qualifierGroupTypeId = attributeCache.EntityTypeQualifierValue.AsInteger(); List <int> inheritedGroupTypeIds = null; using (var rockContext = new RockContext()) { var groupType = new GroupTypeService(rockContext).Get(qualifierGroupTypeId); inheritedGroupTypeIds = groupType.GetAllDependentGroupTypeIds(rockContext); } if (inheritedGroupTypeIds != null) { foreach (var inheritedGroupTypeId in inheritedGroupTypeIds) { Expression inheritedEntityQualiferColumnEqualExpression = Expression.Equal(entityQualiferColumnExpression, Expression.Constant(inheritedGroupTypeId)); entityQualiferColumnEqualExpression = Expression.Or(entityQualiferColumnEqualExpression, inheritedEntityQualiferColumnEqualExpression); } } } expression = Expression.And(entityQualiferColumnEqualExpression, expression); } } } // If we have used an inverted comparison type for the evaluation, invert the Expression so that it excludes the matching Entities. if (comparisonType != evaluatedComparisonType) { return(Expression.Not(expression)); } else { return(expression); } }