Skip to content

Instantly share code, notes, and snippets.

@filipw
Last active March 4, 2024 04:17
Show Gist options
  • Star 31 You must be signed in to star a gist
  • Fork 8 You must be signed in to fork a gist
  • Save filipw/4511410 to your computer and use it in GitHub Desktop.
Save filipw/4511410 to your computer and use it in GitHub Desktop.
removed unnecessary classes
using System;
using System.Linq.Expressions;
using System.Reflection;
using WebApi.Delta;
namespace Hst.Deals.API.Infrastructure
{
internal class CompiledPropertyAccessor<TEntityType> : PropertyAccessor<TEntityType> where TEntityType : class
{
private Action<TEntityType, object> _setter;
private Func<TEntityType, object> _getter;
public CompiledPropertyAccessor(PropertyInfo property)
: base(property)
{
_setter = MakeSetter(Property);
_getter = MakeGetter(Property);
}
public override object GetValue(TEntityType entity)
{
if (entity == null)
{
throw new ArgumentNullException("entity");
}
return _getter(entity);
}
public override void SetValue(TEntityType entity, object value)
{
if (entity == null)
{
throw new ArgumentNullException("entity");
}
_setter(entity, value);
}
private static Action<TEntityType, object> MakeSetter(PropertyInfo property)
{
Type type = typeof(TEntityType);
ParameterExpression entityParameter = Expression.Parameter(type);
ParameterExpression objectParameter = Expression.Parameter(typeof(Object));
MemberExpression toProperty = Expression.Property(Expression.TypeAs(entityParameter, property.DeclaringType), property);
UnaryExpression fromValue = Expression.Convert(objectParameter, property.PropertyType);
BinaryExpression assignment = Expression.Assign(toProperty, fromValue);
Expression<Action<TEntityType, object>> lambda = Expression.Lambda<Action<TEntityType, object>>(assignment, entityParameter, objectParameter);
return lambda.Compile();
}
private static Func<TEntityType, object> MakeGetter(PropertyInfo property)
{
Type type = typeof(TEntityType);
ParameterExpression entityParameter = Expression.Parameter(type);
MemberExpression fromProperty = Expression.Property(Expression.TypeAs(entityParameter, property.DeclaringType), property);
UnaryExpression convert = Expression.Convert(fromProperty, typeof(Object));
Expression<Func<TEntityType, object>> lambda = Expression.Lambda<Func<TEntityType, object>>(convert, entityParameter);
return lambda.Compile();
}
}
}
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Dynamic;
using System.Linq;
using System.Reflection;
using Hst.Deals.API.Infrastructure;
//adapted from https://github.com/ASP-NET-MVC/aspnetwebstack/tree/master/src/System.Web.Http.OData/OData
namespace WebApi.Delta.Models
{
/// <summary>
/// A class the tracks changes (i.e. the Delta) for a particular <typeparamref name="TEntityType"/>.
/// </summary>
/// <typeparam name="TEntityType">TEntityType is the base type of entity this delta tracks changes for.</typeparam>
[NonValidatingParameterBinding]
public class Delta<TEntityType> : DynamicObject where TEntityType : class
{
// cache property accessors for this type and all its derived types.
private static ConcurrentDictionary<Type, Dictionary<string, PropertyAccessor<TEntityType>>> _propertyCache = new ConcurrentDictionary<Type, Dictionary<string, PropertyAccessor<TEntityType>>>();
private Dictionary<string, PropertyAccessor<TEntityType>> _propertiesThatExist;
private HashSet<string> _changedProperties;
private TEntityType _entity;
private Type _entityType;
/// <summary>
/// Initializes a new instance of <see cref="Delta{TEntityType}"/>.
/// </summary>
public Delta()
: this(typeof(TEntityType))
{
}
/// <summary>
/// Initializes a new instance of <see cref="Delta{TEntityType}"/>.
/// </summary>
/// <param name="entityType">The derived entity type for which the changes would be tracked.
/// <paramref name="entityType"/> should be assignable to instances of <typeparamref name="TEntityType"/>.</param>
public Delta(Type entityType)
{
Initialize(entityType);
}
/// <summary>
/// The actual type of the entity for which the changes are tracked.
/// </summary>
public Type EntityType
{
get
{
return _entityType;
}
}
/// <summary>
/// Clears the Delta and resets the underlying Entity.
/// </summary>
public void Clear()
{
Initialize(_entityType);
}
/// <summary>
/// Attempts to set the Property called <paramref name="name"/> to the <paramref name="value"/> specified.
/// <remarks>
/// Only properties that exist on <see cref="EntityType"/> can be set.
/// If there is a type mismatch the request will fail.
/// </remarks>
/// </summary>
/// <param name="name">The name of the Property</param>
/// <param name="value">The new value of the Property</param>
/// <returns>True if successful</returns>
public bool TrySetPropertyValue(string name, object value)
{
if (name == null)
{
throw new ArgumentNullException("name");
}
if (!_propertiesThatExist.ContainsKey(name))
{
return false;
}
PropertyAccessor<TEntityType> cacheHit = _propertiesThatExist[name];
var isGuid = cacheHit.Property.PropertyType == typeof(Guid) && value is string;
var isInt32 = cacheHit.Property.PropertyType == typeof(int) && value is Int64 && (long)value <= int.MaxValue;
if (value == null && !IsNullable(cacheHit.Property.PropertyType))
{
return false;
}
if (value != null && !cacheHit.Property.PropertyType.IsPrimitive && !isGuid && !cacheHit.Property.PropertyType.IsAssignableFrom(value.GetType()))
{
return false;
}
//.Setter.Invoke(_entity, new object[] { value });
if (isGuid) value = new Guid((string)value);
if (isInt32) value = (int)(long)value;
cacheHit.SetValue(_entity, value);
_changedProperties.Add(name);
return true;
}
/// <summary>
/// Attempts to get the value of the Property called <paramref name="name"/> from the underlying Entity.
/// <remarks>
/// Only properties that exist on <see cref="EntityType"/> can be retrieved.
/// Both modified and unmodified properties can be retrieved.
/// </remarks>
/// </summary>
/// <param name="name">The name of the Property</param>
/// <param name="value">The value of the Property</param>
/// <returns>True if the Property was found</returns>
public bool TryGetPropertyValue(string name, out object value)
{
if (name == null)
{
throw new ArgumentNullException("name");
}
if (_propertiesThatExist.ContainsKey(name))
{
PropertyAccessor<TEntityType> cacheHit = _propertiesThatExist[name];
value = cacheHit.GetValue(_entity);
return true;
}
else
{
value = null;
return false;
}
}
/// <summary>
/// Attempts to get the <see cref="Type"/> of the Property called <paramref name="name"/> from the underlying Entity.
/// <remarks>
/// Only properties that exist on <see cref="EntityType"/> can be retrieved.
/// Both modified and unmodified properties can be retrieved.
/// </remarks>
/// </summary>
/// <param name="name">The name of the Property</param>
/// <param name="type">The type of the Property</param>
/// <returns>Returns <c>true</c> if the Property was found and <c>false</c> if not.</returns>
public bool TryGetPropertyType(string name, out Type type)
{
if (name == null)
{
throw new ArgumentNullException("name");
}
PropertyAccessor<TEntityType> value;
if (_propertiesThatExist.TryGetValue(name, out value))
{
type = value.Property.PropertyType;
return true;
}
else
{
type = null;
return false;
}
}
/// <summary>
/// Overrides the DynamicObject TrySetMember method, so that only the properties
/// of <see cref="EntityType"/> can be set.
/// </summary>
public override bool TrySetMember(SetMemberBinder binder, object value)
{
if (binder == null)
{
throw new ArgumentNullException("binder");
}
return TrySetPropertyValue(binder.Name, value);
}
/// <summary>
/// Overrides the DynamicObject TryGetMember method, so that only the properties
/// of <see cref="EntityType"/> can be got.
/// </summary>
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
if (binder == null)
{
throw new ArgumentNullException("binder");
}
return TryGetPropertyValue(binder.Name, out result);
}
/// <summary>
/// Returns the <see cref="EntityType"/> instance
/// that holds all the changes (and original values) being tracked by this Delta.
/// </summary>
[SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "Not appropriate to be a property")]
public TEntityType GetEntity()
{
return _entity;
}
/// <summary>
/// Returns the Properties that have been modified through this Delta as an
/// enumeration of Property Names
/// </summary>
public IEnumerable<string> GetChangedPropertyNames()
{
return _changedProperties;
}
/// <summary>
/// Returns the Properties that have not been modified through this Delta as an
/// enumeration of Property Names
/// </summary>
public IEnumerable<string> GetUnchangedPropertyNames()
{
return _propertiesThatExist.Keys.Except(GetChangedPropertyNames());
}
/// <summary>
/// Copies the changed property values from the underlying entity (accessible via <see cref="GetEntity()" />)
/// to the <paramref name="original"/> entity.
/// </summary>
/// <param name="original">The entity to be updated.</param>
public void CopyChangedValues(TEntityType original)
{
if (original == null)
{
throw new ArgumentNullException("original");
}
if (!_entityType.IsAssignableFrom(original.GetType()))
{
throw new ArgumentException("Delta type mismatch", "original");
}
PropertyAccessor<TEntityType>[] propertiesToCopy = GetChangedPropertyNames().Select(s => _propertiesThatExist[s]).ToArray();
foreach (PropertyAccessor<TEntityType> propertyToCopy in propertiesToCopy)
{
propertyToCopy.Copy(_entity, original);
}
}
/// <summary>
/// Copies the unchanged property values from the underlying entity (accessible via <see cref="GetEntity()" />)
/// to the <paramref name="original"/> entity.
/// </summary>
/// <param name="original">The entity to be updated.</param>
public void CopyUnchangedValues(TEntityType original)
{
if (original == null)
{
throw new ArgumentNullException("original");
}
if (!_entityType.IsAssignableFrom(original.GetType()))
{
//throw Error.Argument("original", SRResources.DeltaTypeMismatch, _entityType, original.GetType());
throw new ArgumentException("Delta type mismatch", "original");
}
PropertyAccessor<TEntityType>[] propertiesToCopy = GetUnchangedPropertyNames().Select(s => _propertiesThatExist[s]).ToArray();
foreach (PropertyAccessor<TEntityType> propertyToCopy in propertiesToCopy)
{
propertyToCopy.Copy(_entity, original);
}
}
/// <summary>
/// Overwrites the <paramref name="original"/> entity with the changes tracked by this Delta.
/// <remarks>The semantics of this operation are equivalent to a HTTP PATCH operation, hence the name.</remarks>
/// </summary>
/// <param name="original">The entity to be updated.</param>
public void Patch(TEntityType original)
{
CopyChangedValues(original);
}
/// <summary>
/// Overwrites the <paramref name="original"/> entity with the values stored in this Delta.
/// <remarks>The semantics of this operation are equivalent to a HTTP PUT operation, hence the name.</remarks>
/// </summary>
/// <param name="original">The entity to be updated.</param>
public void Put(TEntityType original)
{
CopyChangedValues(original);
CopyUnchangedValues(original);
}
private void Initialize(Type entityType)
{
if (entityType == null)
{
throw new ArgumentNullException("entityType");
}
if (!typeof(TEntityType).IsAssignableFrom(entityType))
{
//throw Error.InvalidOperation(SRResources.DeltaEntityTypeNotAssignable, entityType, typeof(TEntityType));
throw new InvalidOperationException("Delta Entity Type Not Assignable");
}
_entity = Activator.CreateInstance(entityType) as TEntityType;
_changedProperties = new HashSet<string>();
_entityType = entityType;
_propertiesThatExist = InitializePropertiesThatExist();
}
private Dictionary<string, PropertyAccessor<TEntityType>> InitializePropertiesThatExist()
{
return _propertyCache.GetOrAdd(
_entityType,
(backingType) => backingType
.GetProperties()
.Where(p => p.GetSetMethod() != null && p.GetGetMethod() != null)
.Select<PropertyInfo, PropertyAccessor<TEntityType>>(p => new CompiledPropertyAccessor<TEntityType>(p))
.ToDictionary(p => p.Property.Name));
}
public static bool IsNullable(Type type)
{
return !type.IsValueType || Nullable.GetUnderlyingType(type) != null;
}
}
}
using System.Collections.Generic;
using System.Net.Http.Formatting;
using System.Web.Http;
using System.Web.Http.Controllers;
namespace WebApi.Delta
{
internal sealed class NonValidatingParameterBindingAttribute : ParameterBindingAttribute
{
public override HttpParameterBinding GetBinding(HttpParameterDescriptor parameter)
{
IEnumerable<MediaTypeFormatter> formatters = parameter.Configuration.Formatters;
return new NonValidatingParameterBinding(parameter, formatters);
}
private sealed class NonValidatingParameterBinding : PerRequestParameterBinding
{
public NonValidatingParameterBinding(HttpParameterDescriptor descriptor,
IEnumerable<MediaTypeFormatter> formatters)
: base(descriptor, formatters)
{
}
protected override HttpParameterBinding CreateInnerBinding(IEnumerable<MediaTypeFormatter> perRequestFormatters)
{
return Descriptor.BindWithFormatter(perRequestFormatters, bodyModelValidator: null);
}
}
}
}
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Data.Linq;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
using System.Globalization;
using System.Net.Http.Formatting;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Http.Controllers;
using System.Web.Http.Metadata;
using System.Web.Http.ModelBinding;
using System.Web.Http.ValueProviders;
using System.Xml.Linq;
namespace WebApi.Delta
{
internal class PerRequestParameterBinding : HttpParameterBinding
{
private IEnumerable<MediaTypeFormatter> _formatters;
public PerRequestParameterBinding(HttpParameterDescriptor descriptor,
IEnumerable<MediaTypeFormatter> formatters)
: base(descriptor)
{
if (formatters == null)
{
throw new ArgumentNullException("formatters");
}
_formatters = formatters;
}
public override Task ExecuteBindingAsync(ModelMetadataProvider metadataProvider, HttpActionContext actionContext,
CancellationToken cancellationToken)
{
List<MediaTypeFormatter> perRequestFormatters = new List<MediaTypeFormatter>();
foreach (MediaTypeFormatter formatter in _formatters)
{
MediaTypeFormatter perRequestFormatter =
formatter.GetPerRequestFormatterInstance(Descriptor.ParameterType, actionContext.Request,
actionContext.Request.Content.Headers.ContentType);
perRequestFormatters.Add(perRequestFormatter);
}
HttpParameterBinding innerBinding = CreateInnerBinding(perRequestFormatters);
Contract.Assert(innerBinding != null);
return innerBinding.ExecuteBindingAsync(metadataProvider, actionContext, cancellationToken);
}
protected virtual HttpParameterBinding CreateInnerBinding(IEnumerable<MediaTypeFormatter> perRequestFormatters)
{
return Descriptor.BindWithFormatter(perRequestFormatters);
}
}
}
using System;
using System.Reflection;
namespace WebApi.Delta
{
internal abstract class PropertyAccessor<TEntityType> where TEntityType : class
{
protected PropertyAccessor(PropertyInfo property)
{
if (property == null)
{
throw new ArgumentNullException("property");
}
Property = property;
if (Property.GetGetMethod() == null || Property.GetSetMethod() == null)
{
throw new ArgumentException("Property Must Have Public Getter And Setter", "property");
}
}
public PropertyInfo Property
{
get;
private set;
}
public void Copy(TEntityType from, TEntityType to)
{
if (from == null)
{
throw new ArgumentNullException("from");
}
if (to == null)
{
throw new ArgumentNullException("to");
}
SetValue(to, GetValue(from));
}
public abstract object GetValue(TEntityType entity);
public abstract void SetValue(TEntityType entity, object value);
}
}
@lijunle
Copy link

lijunle commented Dec 17, 2014

Add following snippet to Delta.cs to support parse enum:

// enum type
if (value != null && cacheHit.Property.PropertyType.IsEnum)
{
    try
    {
        value = Enum.Parse(cacheHit.Property.PropertyType, value.ToString(), true);
    }
    catch (ArgumentException)
    {
        // value is not a member of enum
        return false;
    }
}

@rsgilbert
Copy link

Dont forget to install Microsoft.AspNet.WebApi.Core. Its relied on by these classes.

Do

dotnet add package Microsoft.AspNet.WebApi.Core

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment