Skip to content

Instantly share code, notes, and snippets.

@msmorgan
Created February 24, 2017 02:03
Show Gist options
  • Save msmorgan/e1143f66d78bb8496b9b9a22c3dc87c1 to your computer and use it in GitHub Desktop.
Save msmorgan/e1143f66d78bb8496b9b9a22c3dc87c1 to your computer and use it in GitHub Desktop.
API demonstration of Calculated Properties proposal
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