Created
February 24, 2017 02:03
-
-
Save msmorgan/e1143f66d78bb8496b9b9a22c3dc87c1 to your computer and use it in GitHub Desktop.
API demonstration of Calculated Properties proposal
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.ComponentModel.DataAnnotations; | |
using System.ComponentModel.DataAnnotations.Schema; | |
using System.Data.Entity; | |
using System.Linq; | |
using System.Linq.Expressions; | |
using System.Reflection; | |
namespace CalculatedProperties | |
{ | |
public class UsageDemo | |
{ | |
public static void Main(string[] args) | |
{ | |
var dbSet = GetDbSet<ParentEntity>(); | |
var results = dbSet | |
// calculated properties are translated appropriately when used in expressions | |
.Where(m => m.TotalValue > 100) | |
// Calculate works similarly to Include | |
.Calculate(pe => pe.DisplayName) | |
.Calculate(pe => pe.ChildCount) | |
.ToList(); | |
Console.WriteLine("high-value ParentEntities:"); | |
foreach (var pe in results) | |
{ | |
Console.WriteLine($"{pe.DisplayName} ({pe.ChildCount} children)"); | |
} | |
} | |
private static DbSet<TEntity> GetDbSet<TEntity>() | |
where TEntity : class | |
{ | |
throw new NotImplementedException(); | |
} | |
} | |
public class ParentEntity | |
{ | |
#region Mapped properties | |
[Key] | |
[DatabaseGenerated(DatabaseGeneratedOption.Identity)] | |
public int Id { get; set; } | |
[Required] | |
public string ItemCode { get; set; } | |
[Required] | |
public string Description { get; set; } | |
#endregion | |
#region Calculated properties | |
#region DisplayName | |
public static readonly Expression<Func<ParentEntity, string>> DisplayNameExpression = | |
pe => pe.ItemCode + ": " + pe.Description; | |
// It should be possible to use conventions to configure calculated properties without using this attribute, | |
// as long as a static property matching the configuration exists. | |
[Calculated(nameof(DisplayNameExpression))] | |
public CalculationResult<string> DisplayName { get; set; } | |
#endregion | |
#region TotalValue | |
public static readonly Expression<Func<ParentEntity, double>> TotalValueExpression = | |
pe => pe.Children.Sum(c => (double?) c.Value) ?? 0; | |
[Calculated(nameof(TotalValueExpression))] | |
public CalculationResult<double> TotalValue { get; set; } | |
#endregion | |
#region ChildCount | |
public static readonly Expression<Func<ParentEntity, int>> ChildCountExpression = | |
pe => pe.Children.Count(); | |
[Calculated(nameof(ChildCountExpression))] | |
public CalculationResult<int> ChildCount { get; set; } | |
#endregion | |
#endregion | |
#region Navigation properties | |
[InverseProperty(nameof(ChildEntity.Parent))] | |
public ICollection<ChildEntity> Children { get; set; } | |
#endregion | |
} | |
public class ChildEntity | |
{ | |
#region Mapped properties | |
[Key] | |
[DatabaseGenerated(DatabaseGeneratedOption.Identity)] | |
public int Id { get; set; } | |
public int ParentId { get; set; } | |
public double FactorA { get; set; } | |
public double FactorB { get; set; } | |
#endregion | |
#region Calculated properties | |
#region Value | |
public static readonly Expression<Func<ChildEntity, double>> ValueExpression = | |
c => c.FactorA * c.FactorB; | |
[Calculated(nameof(ValueExpression))] | |
public CalculationResult<double> Value { get; set; } | |
#endregion | |
#endregion | |
#region Navigation properties | |
[ForeignKey(nameof(ParentId))] | |
public ParentEntity Parent { get; set; } | |
#endregion | |
} | |
// this could be a struct instead, perhaps | |
public class CalculationResult<T> | |
{ | |
private readonly T _value; | |
public CalculationResult() { } | |
public CalculationResult(T value) | |
{ | |
_value = value; | |
HasValue = true; | |
} | |
public bool HasValue { get; } | |
public T Value | |
{ | |
get | |
{ | |
if (!HasValue) | |
{ | |
throw new InvalidOperationException("Cannot get value: no value"); | |
} | |
return _value; | |
} | |
} | |
public override string ToString() | |
{ | |
return HasValue ? "" + Value : "(no value)"; | |
} | |
public static implicit operator T(CalculationResult<T> result) | |
{ | |
return result.Value; | |
} | |
} | |
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] | |
public class CalculatedAttribute : Attribute | |
{ | |
private readonly string _expressionPropertyName; | |
public CalculatedAttribute(string expressionPropertyName) | |
{ | |
_expressionPropertyName = expressionPropertyName; | |
} | |
public Expression GetExpression(Type entityType) | |
{ | |
const MemberTypes memberTypes = MemberTypes.Property | MemberTypes.Field; | |
const BindingFlags bindingFlags = BindingFlags.Static | BindingFlags.FlattenHierarchy; | |
var memberInfo = entityType.GetMember(_expressionPropertyName, memberTypes, bindingFlags).FirstOrDefault(); | |
var propertyInfo = memberInfo as PropertyInfo; | |
if (propertyInfo != null) | |
{ | |
EnsureExpressionType(propertyInfo.PropertyType); | |
return (Expression) propertyInfo.GetValue(null); | |
} | |
var fieldInfo = memberInfo as FieldInfo; | |
if (fieldInfo != null) | |
{ | |
EnsureExpressionType(fieldInfo.FieldType); | |
return (Expression) fieldInfo.GetValue(null); | |
} | |
throw new Exception( | |
$"Could not find a static property/field of '{entityType.Name}' with name '{_expressionPropertyName}'."); | |
} | |
private static bool IsExpressionType(Type type) | |
{ | |
return type == typeof(Expression) || type == typeof(Expression<>); | |
} | |
private static void EnsureExpressionType(Type type) | |
{ | |
if (!IsExpressionType(type)) | |
{ | |
throw new Exception( | |
$"The name supplied to '{nameof(CalculatedAttribute)}' must refer to an Expression or Expression<TFunc>."); | |
} | |
} | |
} | |
public static class CalculationExtensions | |
{ | |
public static IQueryable<TSource> Calculate<TSource, TProperty>( | |
this IQueryable<TSource> source, | |
Expression<Func<TSource, CalculationResult<TProperty>>> propertySelector) | |
{ | |
throw new NotImplementedException(); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment