Imported Upstream version 3.6.0

Former-commit-id: da6be194a6b1221998fc28233f2503bd61dd9d14
This commit is contained in:
Jo Shields
2014-08-13 10:39:27 +01:00
commit a575963da9
50588 changed files with 8155799 additions and 0 deletions

View File

@@ -0,0 +1,35 @@
// Copyright (c) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information.
namespace Microsoft.Web.Http.Data
{
/// <summary>
/// Enumeration of the types of operations a <see cref="DataController"/> can perform.
/// </summary>
public enum ChangeOperation
{
/// <summary>
/// Indicates that no operation is to be performed
/// </summary>
None,
/// <summary>
/// Indicates an operation that inserts new data
/// </summary>
Insert,
/// <summary>
/// Indicates an operation that updates existing data
/// </summary>
Update,
/// <summary>
/// Indicates an operation that deletes existing data
/// </summary>
Delete,
/// <summary>
/// Indicates a custom update operation
/// </summary>
Custom
}
}

View File

@@ -0,0 +1,337 @@
// Copyright (c) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Web.Http;
using System.Web.Http.Controllers;
namespace Microsoft.Web.Http.Data
{
/// <summary>
/// Represents a set of changes to be processed by a <see cref="DataController"/>.
/// </summary>
public sealed class ChangeSet
{
private IEnumerable<ChangeSetEntry> _changeSetEntries;
/// <summary>
/// Initializes a new instance of the ChangeSet class
/// </summary>
/// <param name="changeSetEntries">The set of <see cref="ChangeSetEntry"/> items this <see cref="ChangeSet"/> represents.</param>
/// <exception cref="ArgumentNullException">if <paramref name="changeSetEntries"/> is null.</exception>
public ChangeSet(IEnumerable<ChangeSetEntry> changeSetEntries)
{
if (changeSetEntries == null)
{
throw Error.ArgumentNull("changeSetEntries");
}
// ensure the changeset is valid
ValidateChangeSetEntries(changeSetEntries);
_changeSetEntries = changeSetEntries;
}
/// <summary>
/// Gets the set of <see cref="ChangeSetEntry"/> items this <see cref="ChangeSet"/> represents.
/// </summary>
public ReadOnlyCollection<ChangeSetEntry> ChangeSetEntries
{
get { return _changeSetEntries.ToList().AsReadOnly(); }
}
/// <summary>
/// Gets a value indicating whether any of the <see cref="ChangeSetEntry"/> items has an error.
/// </summary>
public bool HasError
{
get { return _changeSetEntries.Any(op => op.HasConflict || (op.ValidationErrors != null && op.ValidationErrors.Any())); }
}
/// <summary>
/// Returns the original unmodified entity for the provided <paramref name="clientEntity"/>.
/// </summary>
/// <remarks>
/// Note that only members marked with <see cref="RoundtripOriginalAttribute"/> will be set
/// in the returned instance.
/// </remarks>
/// <typeparam name="TEntity">The entity type.</typeparam>
/// <param name="clientEntity">The client modified entity.</param>
/// <returns>The original unmodified entity for the provided <paramref name="clientEntity"/>.</returns>
/// <exception cref="ArgumentNullException">if <paramref name="clientEntity"/> is null.</exception>
/// <exception cref="ArgumentException">if <paramref name="clientEntity"/> is not in the change set.</exception>
public TEntity GetOriginal<TEntity>(TEntity clientEntity) where TEntity : class
{
if (clientEntity == null)
{
throw Error.ArgumentNull("clientEntity");
}
ChangeSetEntry entry = _changeSetEntries.FirstOrDefault(p => Object.ReferenceEquals(p.Entity, clientEntity));
if (entry == null)
{
throw Error.Argument(Resource.ChangeSet_ChangeSetEntryNotFound);
}
if (entry.Operation == ChangeOperation.Insert)
{
throw Error.InvalidOperation(Resource.ChangeSet_OriginalNotValidForInsert);
}
return (TEntity)entry.OriginalEntity;
}
/// <summary>
/// Validates that the specified entries are well formed.
/// </summary>
/// <param name="changeSetEntries">The changeset entries to validate.</param>
private static void ValidateChangeSetEntries(IEnumerable<ChangeSetEntry> changeSetEntries)
{
HashSet<int> idSet = new HashSet<int>();
HashSet<object> entitySet = new HashSet<object>();
foreach (ChangeSetEntry entry in changeSetEntries)
{
// ensure Entity is not null
if (entry.Entity == null)
{
throw Error.InvalidOperation(Resource.InvalidChangeSet, Resource.InvalidChangeSet_NullEntity);
}
// ensure unique client IDs
if (idSet.Contains(entry.Id))
{
throw Error.InvalidOperation(Resource.InvalidChangeSet, Resource.InvalidChangeSet_DuplicateId);
}
idSet.Add(entry.Id);
// ensure unique entity instances - there can only be a single entry
// for a given entity instance
if (entitySet.Contains(entry.Entity))
{
throw Error.InvalidOperation(Resource.InvalidChangeSet, Resource.InvalidChangeSet_DuplicateEntity);
}
entitySet.Add(entry.Entity);
// entities must be of the same type
if (entry.OriginalEntity != null && !(entry.Entity.GetType() == entry.OriginalEntity.GetType()))
{
throw Error.InvalidOperation(Resource.InvalidChangeSet, Resource.InvalidChangeSet_MustBeSameType);
}
if (entry.Operation == ChangeOperation.Insert && entry.OriginalEntity != null)
{
throw Error.InvalidOperation(Resource.InvalidChangeSet, Resource.InvalidChangeSet_InsertsCantHaveOriginal);
}
}
// now that we have the full Id space, we can validate associations
foreach (ChangeSetEntry entry in changeSetEntries)
{
if (entry.Associations != null)
{
ValidateAssociationMap(entry.Entity.GetType(), idSet, entry.Associations);
}
if (entry.OriginalAssociations != null)
{
ValidateAssociationMap(entry.Entity.GetType(), idSet, entry.OriginalAssociations);
}
}
}
/// <summary>
/// Validates the specified association map.
/// </summary>
/// <param name="entityType">The entity type the association is on.</param>
/// <param name="idSet">The set of all unique Ids in the changeset.</param>
/// <param name="associationMap">The association map to validate.</param>
private static void ValidateAssociationMap(Type entityType, HashSet<int> idSet, IDictionary<string, int[]> associationMap)
{
PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(entityType);
foreach (var associationItem in associationMap)
{
// ensure that the member is an association member
string associationMemberName = associationItem.Key;
PropertyDescriptor associationMember = properties[associationMemberName];
if (associationMember == null || associationMember.Attributes[typeof(AssociationAttribute)] == null)
{
throw Error.InvalidOperation(Resource.InvalidChangeSet,
String.Format(CultureInfo.CurrentCulture, Resource.InvalidChangeSet_InvalidAssociationMember, entityType, associationMemberName));
}
// ensure that the id collection is not null
if (associationItem.Value == null)
{
throw Error.InvalidOperation(Resource.InvalidChangeSet,
String.Format(CultureInfo.CurrentCulture, Resource.InvalidChangeSet_AssociatedIdsCannotBeNull, entityType, associationMemberName));
}
// ensure that each Id specified is in the changeset
foreach (int id in associationItem.Value)
{
if (!idSet.Contains(id))
{
throw Error.InvalidOperation(Resource.InvalidChangeSet,
String.Format(CultureInfo.CurrentCulture, Resource.InvalidChangeSet_AssociatedIdNotInChangeset, id, entityType, associationMemberName));
}
}
}
}
/// <summary>
/// Reestablish associations based on Id lists by adding the referenced entities
/// to their association members
/// </summary>
internal void SetEntityAssociations()
{
// create a unique map from Id to entity instances, and update operations
// so Ids map to the same instances, since during deserialization reference
// identity is not maintained.
var entityIdMap = _changeSetEntries.ToDictionary(p => p.Id, p => new { Entity = p.Entity, OriginalEntity = p.OriginalEntity });
foreach (ChangeSetEntry changeSetEntry in _changeSetEntries)
{
object entity = entityIdMap[changeSetEntry.Id].Entity;
if (changeSetEntry.Entity != entity)
{
changeSetEntry.Entity = entity;
}
object original = entityIdMap[changeSetEntry.Id].OriginalEntity;
if (original != null && changeSetEntry.OriginalEntity != original)
{
changeSetEntry.OriginalEntity = original;
}
}
// for all entities with associations, reestablish the associations by mapping the Ids
// to entity instances and adding them to the association members
HashSet<int> visited = new HashSet<int>();
foreach (var entityGroup in _changeSetEntries.Where(p => (p.Associations != null && p.Associations.Count > 0) || (p.OriginalAssociations != null && p.OriginalAssociations.Count > 0)).GroupBy(p => p.Entity.GetType()))
{
Dictionary<string, PropertyDescriptor> associationMemberMap = TypeDescriptor.GetProperties(entityGroup.Key).Cast<PropertyDescriptor>().Where(p => p.Attributes[typeof(AssociationAttribute)] != null).ToDictionary(p => p.Name);
foreach (ChangeSetEntry changeSetEntry in entityGroup)
{
if (visited.Contains(changeSetEntry.Id))
{
continue;
}
visited.Add(changeSetEntry.Id);
// set current associations
if (changeSetEntry.Associations != null)
{
foreach (var associationItem in changeSetEntry.Associations)
{
PropertyDescriptor assocMember = associationMemberMap[associationItem.Key];
IEnumerable<object> children = associationItem.Value.Select(p => entityIdMap[p].Entity);
SetAssociationMember(changeSetEntry.Entity, assocMember, children);
}
}
}
}
}
internal bool Validate(HttpActionContext actionContext)
{
// Validate all entries except those with type None or Delete (since we don't want to validate
// entites we're going to delete).
bool success = true;
IEnumerable<ChangeSetEntry> entriesToValidate = ChangeSetEntries.Where(
p => (p.ActionDescriptor != null && p.Operation != ChangeOperation.None && p.Operation != ChangeOperation.Delete)
|| (p.EntityActions != null && p.EntityActions.Any()));
foreach (ChangeSetEntry entry in entriesToValidate)
{
// TODO: optimize by determining whether a type actually requires any validation?
// TODO: support for method level / parameter validation?
List<ValidationResultInfo> validationErrors = new List<ValidationResultInfo>();
if (!DataControllerValidation.ValidateObject(entry.Entity, validationErrors, actionContext))
{
entry.ValidationErrors = validationErrors.Distinct(EqualityComparer<ValidationResultInfo>.Default).ToList();
success = false;
}
// clear after each validate call, since we've already
// copied over the errors
actionContext.ModelState.Clear();
}
return success;
}
/// <summary>
/// Adds the specified associated entities to the specified association member for the specified entity.
/// </summary>
/// <param name="entity">The entity</param>
/// <param name="associationProperty">The association member (singleton or collection)</param>
/// <param name="associatedEntities">Collection of associated entities</param>
private static void SetAssociationMember(object entity, PropertyDescriptor associationProperty, IEnumerable<object> associatedEntities)
{
if (associatedEntities.Count() == 0)
{
return;
}
object associationValue = associationProperty.GetValue(entity);
if (typeof(IEnumerable).IsAssignableFrom(associationProperty.PropertyType))
{
if (associationValue == null)
{
throw Error.InvalidOperation(Resource.DataController_AssociationCollectionPropertyIsNull, associationProperty.ComponentType.Name, associationProperty.Name);
}
IList list = associationValue as IList;
IEnumerable<object> associationSequence = null;
MethodInfo addMethod = null;
if (list == null)
{
// not an IList, so we have to use reflection
Type associatedEntityType = TypeUtility.GetElementType(associationValue.GetType());
addMethod = associationValue.GetType().GetMethod("Add", BindingFlags.Public | BindingFlags.Instance, null, new Type[] { associatedEntityType }, null);
if (addMethod == null)
{
throw Error.InvalidOperation(Resource.DataController_InvalidCollectionMember, associationProperty.Name);
}
associationSequence = ((IEnumerable)associationValue).Cast<object>();
}
foreach (object associatedEntity in associatedEntities)
{
// add the entity to the collection if it's not already there
if (list != null)
{
if (!list.Contains(associatedEntity))
{
list.Add(associatedEntity);
}
}
else
{
if (!associationSequence.Contains(associatedEntity))
{
addMethod.Invoke(associationValue, new object[] { associatedEntity });
}
}
}
}
else
{
// set the reference if it's not already set
object associatedEntity = associatedEntities.Single();
object currentValue = associationProperty.GetValue(entity);
if (!Object.Equals(currentValue, associatedEntity))
{
associationProperty.SetValue(entity, associatedEntity);
}
}
}
}
}

View File

@@ -0,0 +1,102 @@
// Copyright (c) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.Serialization;
namespace Microsoft.Web.Http.Data
{
/// <summary>
/// Represents a change operation to be performed on an entity.
/// </summary>
[DataContract]
[DebuggerDisplay("Operation = {Operation}, Type = {Entity.GetType().Name}")]
public sealed class ChangeSetEntry
{
/// <summary>
/// Gets or sets the client ID for the entity
/// </summary>
[DataMember]
public int Id { get; set; }
/// <summary>
/// Gets or sets the <see cref="ChangeOperation"/> to be performed on the entity.
/// </summary>
[DataMember]
public ChangeOperation Operation { get; set; }
/// <summary>
/// Gets or sets the <see cref="Entity"/> being operated on
/// </summary>
[DataMember]
public object Entity { get; set; }
/// <summary>
/// Gets or sets the original state of the entity being operated on
/// </summary>
[DataMember(EmitDefaultValue = false)]
public object OriginalEntity { get; set; }
/// <summary>
/// Gets or sets the state of the entity in the data store
/// </summary>
[DataMember(EmitDefaultValue = false)]
public object StoreEntity { get; set; }
/// <summary>
/// Gets or sets the custom methods invoked on the entity, as a set
/// of method name / parameter set pairs.
/// </summary>
[DataMember(EmitDefaultValue = false)]
public IDictionary<string, object[]> EntityActions { get; set; }
/// <summary>
/// Gets or sets the validation errors encountered during the processing of the operation.
/// </summary>
[DataMember(EmitDefaultValue = false)]
public IEnumerable<ValidationResultInfo> ValidationErrors { get; set; }
/// <summary>
/// Gets or sets the collection of members in conflict. The <see cref="StoreEntity"/> property
/// contains the current store value for each member in conflict.
/// </summary>
[DataMember(EmitDefaultValue = false)]
public IEnumerable<string> ConflictMembers { get; set; }
/// <summary>
/// Gets or sets whether the conflict is a delete conflict, meaning the
/// entity no longer exists in the store.
/// </summary>
[DataMember(EmitDefaultValue = false)]
public bool IsDeleteConflict { get; set; }
/// <summary>
/// Gets or sets the collection of IDs of the associated entities for
/// each association of the Entity
/// </summary>
[DataMember(EmitDefaultValue = false)]
public IDictionary<string, int[]> Associations { get; set; }
/// <summary>
/// Gets or sets the collection of IDs for each association of the <see cref="OriginalEntity"/>
/// </summary>
[DataMember(EmitDefaultValue = false)]
public IDictionary<string, int[]> OriginalAssociations { get; set; }
/// <summary>
/// Gets a value indicating whether the <see cref="ChangeSetEntry"/> contains conflicts.
/// </summary>
public bool HasConflict
{
get { return (IsDeleteConflict || (ConflictMembers != null && ConflictMembers.Any())); }
}
public bool HasError
{
get { return HasConflict || (ValidationErrors != null && ValidationErrors.Any()); }
}
internal UpdateActionDescriptor ActionDescriptor { get; set; }
}
}

View File

@@ -0,0 +1,74 @@
// Copyright (c) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using System.Web.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;
namespace Microsoft.Web.Http.Data
{
/// <summary>
/// A wrapper <see cref="HttpActionDescriptor"/> that customizes various aspects of the wrapped
/// inner descriptor, for example by adding additional action filters.
/// </summary>
internal sealed class CustomizingActionDescriptor : HttpActionDescriptor
{
private HttpActionDescriptor _innerDescriptor;
public CustomizingActionDescriptor(HttpActionDescriptor innerDescriptor)
{
_innerDescriptor = innerDescriptor;
Configuration = _innerDescriptor.Configuration;
ControllerDescriptor = _innerDescriptor.ControllerDescriptor;
}
public override string ActionName
{
get { return _innerDescriptor.ActionName; }
}
public override Type ReturnType
{
get { return _innerDescriptor.ReturnType; }
}
public override IActionResultConverter ResultConverter
{
get { return _innerDescriptor.ResultConverter; }
}
public override Task<object> ExecuteAsync(HttpControllerContext controllerContext, IDictionary<string, object> arguments)
{
return _innerDescriptor.ExecuteAsync(controllerContext, arguments);
}
public override Collection<HttpParameterDescriptor> GetParameters()
{
return _innerDescriptor.GetParameters();
}
public override Collection<FilterInfo> GetFilterPipeline()
{
Collection<FilterInfo> originalFilters = _innerDescriptor.GetFilterPipeline();
Collection<FilterInfo> newFilters = new Collection<FilterInfo>();
// for any actions that support query composition, we need to replace it with our
// query filter.
foreach (FilterInfo filterInfo in originalFilters)
{
FilterInfo newInfo = filterInfo;
QueryableAttribute queryableFilter = filterInfo.Instance as QueryableAttribute;
if (queryableFilter != null)
{
newInfo = new FilterInfo(new QueryFilterAttribute() { ResultLimit = queryableFilter.ResultLimit }, filterInfo.Scope);
}
newFilters.Add(newInfo);
}
return newFilters;
}
}
}

View File

@@ -0,0 +1,354 @@
// Copyright (c) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Http;
using System.Web.Http.Controllers;
namespace Microsoft.Web.Http.Data
{
[HttpControllerConfiguration(HttpActionInvoker = typeof(DataControllerActionInvoker), HttpActionSelector = typeof(DataControllerActionSelector), ActionValueBinder = typeof(DataControllerActionValueBinder))]
public abstract class DataController : ApiController
{
private ChangeSet _changeSet;
private DataControllerDescription _description;
/// <summary>
/// Gets the current <see cref="ChangeSet"/>. Returns null if no change operations are being performed.
/// </summary>
protected ChangeSet ChangeSet
{
get { return _changeSet; }
}
/// <summary>
/// Gets the <see cref="DataControllerDescription"/> for this <see cref="DataController"/>.
/// </summary>
protected DataControllerDescription Description
{
get { return _description; }
}
/// <summary>
/// Gets the <see cref="HttpActionContext"/> for the currently executing action.
/// </summary>
protected internal HttpActionContext ActionContext { get; internal set; }
protected override void Initialize(HttpControllerContext controllerContext)
{
// ensure that the service is valid and all custom metadata providers
// have been registered
_description = DataControllerDescription.GetDescription(controllerContext.ControllerDescriptor);
base.Initialize(controllerContext);
}
/// <summary>
/// Performs the operations indicated by the specified <see cref="ChangeSet"/> by invoking
/// the corresponding actions for each.
/// </summary>
/// <param name="changeSet">The changeset to submit</param>
/// <returns>True if the submit was successful, false otherwise.</returns>
public virtual bool Submit(ChangeSet changeSet)
{
if (changeSet == null)
{
throw Error.ArgumentNull("changeSet");
}
_changeSet = changeSet;
ResolveActions(_description, ChangeSet.ChangeSetEntries);
if (!AuthorizeChangeSet())
{
// Don't try to save if there were any errors.
return false;
}
// Before invoking any operations, validate the entire changeset
if (!ValidateChangeSet())
{
return false;
}
// Now that we're validated, proceed to invoke the actions.
if (!ExecuteChangeSet())
{
return false;
}
// persist the changes
if (!PersistChangeSetInternal())
{
return false;
}
return true;
}
[SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Caller is responsible for the lifetime of the object")]
public override Task<HttpResponseMessage> ExecuteAsync(HttpControllerContext controllerContext, CancellationToken cancellationToken)
{
return base.ExecuteAsync(controllerContext, cancellationToken)
.Then<HttpResponseMessage, HttpResponseMessage>(response =>
{
int totalCount;
if (response != null &&
controllerContext.Request.Properties.TryGetValue<int>(QueryFilterAttribute.TotalCountKey, out totalCount))
{
ObjectContent objectContent = response.Content as ObjectContent;
IEnumerable results;
if (objectContent != null && (results = objectContent.Value as IEnumerable) != null)
{
HttpResponseMessage oldResponse = response;
// Client has requested the total count, so the actual response content will contain
// the query results as well as the count. Create a new ObjectContent for the query results.
// Because this code does not specify any formatters explicitly, it will use the
// formatters in the configuration.
QueryResult queryResult = new QueryResult(results, totalCount);
response = response.RequestMessage.CreateResponse(oldResponse.StatusCode, queryResult);
foreach (var header in oldResponse.Headers)
{
response.Headers.Add(header.Key, header.Value);
}
// TODO what about content headers?
oldResponse.RequestMessage = null;
oldResponse.Dispose();
}
}
return response;
});
}
/// <summary>
/// For all operations in the current changeset, validate that the operation exists, and
/// set the operation entry.
/// </summary>
internal static void ResolveActions(DataControllerDescription description, IEnumerable<ChangeSetEntry> changeSet)
{
// Resolve and set the action for each operation in the changeset
foreach (ChangeSetEntry changeSetEntry in changeSet)
{
Type entityType = changeSetEntry.Entity.GetType();
UpdateActionDescriptor actionDescriptor = null;
if (changeSetEntry.Operation == ChangeOperation.Insert ||
changeSetEntry.Operation == ChangeOperation.Update ||
changeSetEntry.Operation == ChangeOperation.Delete)
{
actionDescriptor = description.GetUpdateAction(entityType, changeSetEntry.Operation);
}
// if a custom method invocation is specified, validate that the method exists
bool isCustomUpdate = false;
if (changeSetEntry.EntityActions != null && changeSetEntry.EntityActions.Any())
{
var entityAction = changeSetEntry.EntityActions.Single();
UpdateActionDescriptor customMethodOperation = description.GetCustomMethod(entityType, entityAction.Key);
if (customMethodOperation == null)
{
throw Error.InvalidOperation(Resource.DataController_InvalidAction, entityAction.Key, entityType.Name);
}
// if the primary action for an update is null but the entry
// contains a valid custom update action, its considered a "custom update"
isCustomUpdate = actionDescriptor == null && customMethodOperation != null;
}
if (actionDescriptor == null && !isCustomUpdate)
{
throw Error.InvalidOperation(Resource.DataController_InvalidAction, changeSetEntry.Operation.ToString(), entityType.Name);
}
changeSetEntry.ActionDescriptor = actionDescriptor;
}
}
/// <summary>
/// Verifies the user is authorized to submit the current <see cref="ChangeSet"/>.
/// </summary>
/// <returns>True if the <see cref="ChangeSet"/> is authorized, false otherwise.</returns>
protected virtual bool AuthorizeChangeSet()
{
foreach (ChangeSetEntry changeSetEntry in ChangeSet.ChangeSetEntries)
{
if (!changeSetEntry.ActionDescriptor.Authorize(ActionContext))
{
return false;
}
// if there are any custom method invocations for this operation
// we need to authorize them as well
if (changeSetEntry.EntityActions != null && changeSetEntry.EntityActions.Any())
{
Type entityType = changeSetEntry.Entity.GetType();
foreach (var entityAction in changeSetEntry.EntityActions)
{
UpdateActionDescriptor customAction = Description.GetCustomMethod(entityType, entityAction.Key);
if (!customAction.Authorize(ActionContext))
{
return false;
}
}
}
}
return !ChangeSet.HasError;
}
/// <summary>
/// Validates the current <see cref="ChangeSet"/>. Any errors should be set on the individual <see cref="ChangeSetEntry"/>s
/// in the <see cref="ChangeSet"/>.
/// </summary>
/// <returns><c>True</c> if all operations in the <see cref="ChangeSet"/> passed validation, <c>false</c> otherwise.</returns>
protected virtual bool ValidateChangeSet()
{
return ChangeSet.Validate(ActionContext);
}
/// <summary>
/// This method invokes the action for each operation in the current <see cref="ChangeSet"/>.
/// </summary>
/// <returns>True if the <see cref="ChangeSet"/> was processed successfully, false otherwise.</returns>
protected virtual bool ExecuteChangeSet()
{
InvokeCUDOperations();
InvokeCustomUpdateOperations();
return !ChangeSet.HasError;
}
private void InvokeCUDOperations()
{
foreach (ChangeSetEntry changeSetEntry in ChangeSet.ChangeSetEntries
.Where(op => op.Operation == ChangeOperation.Insert ||
op.Operation == ChangeOperation.Update ||
op.Operation == ChangeOperation.Delete))
{
if (changeSetEntry.ActionDescriptor == null)
{
continue;
}
InvokeAction(changeSetEntry.ActionDescriptor, new object[] { changeSetEntry.Entity }, changeSetEntry);
}
}
private void InvokeCustomUpdateOperations()
{
foreach (ChangeSetEntry changeSetEntry in ChangeSet.ChangeSetEntries.Where(op => op.EntityActions != null && op.EntityActions.Any()))
{
Type entityType = changeSetEntry.Entity.GetType();
foreach (var entityAction in changeSetEntry.EntityActions)
{
UpdateActionDescriptor customUpdateAction = Description.GetCustomMethod(entityType, entityAction.Key);
List<object> customMethodParams = new List<object>(entityAction.Value);
customMethodParams.Insert(0, changeSetEntry.Entity);
InvokeAction(customUpdateAction, customMethodParams.ToArray(), changeSetEntry);
}
}
}
private void InvokeAction(HttpActionDescriptor action, object[] parameters, ChangeSetEntry changeSetEntry)
{
try
{
Collection<HttpParameterDescriptor> pds = action.GetParameters();
Dictionary<string, object> paramMap = new Dictionary<string, object>(pds.Count);
for (int i = 0; i < pds.Count; i++)
{
paramMap.Add(pds[i].ParameterName, parameters[i]);
}
// TODO this method is not correctly observing the execution results, the catch block below is wrong. 385801
action.ExecuteAsync(ActionContext.ControllerContext, paramMap);
}
catch (TargetInvocationException tie)
{
ValidationException vex = tie.GetBaseException() as ValidationException;
if (vex != null)
{
ValidationResultInfo error = new ValidationResultInfo(vex.Message, 0, String.Empty, vex.ValidationResult.MemberNames);
if (changeSetEntry.ValidationErrors != null)
{
changeSetEntry.ValidationErrors = changeSetEntry.ValidationErrors.Concat(new ValidationResultInfo[] { error }).ToArray();
}
else
{
changeSetEntry.ValidationErrors = new ValidationResultInfo[] { error };
}
}
else
{
throw;
}
}
}
/// <summary>
/// This method is called to finalize changes after all the operations in the current <see cref="ChangeSet"/>
/// have been invoked. This method should commit the changes as necessary to the data store.
/// Any errors should be set on the individual <see cref="ChangeSetEntry"/>s in the <see cref="ChangeSet"/>.
/// </summary>
/// <returns>True if the <see cref="ChangeSet"/> was persisted successfully, false otherwise.</returns>
protected virtual bool PersistChangeSet()
{
return true;
}
/// <summary>
/// This method invokes the user overridable <see cref="PersistChangeSet"/> method wrapping the call
/// with the appropriate exception handling logic. All framework calls to <see cref="PersistChangeSet"/>
/// must go through this method. Some data sources have their own validation hook points,
/// so if a <see cref="ValidationException"/> is thrown at that level, we want to capture it.
/// </summary>
/// <returns>True if the <see cref="ChangeSet"/> was persisted successfully, false otherwise.</returns>
private bool PersistChangeSetInternal()
{
try
{
PersistChangeSet();
}
catch (ValidationException e)
{
// if a validation exception is thrown for one of the entities in the changeset
// set the error on the corresponding ChangeSetEntry
if (e.Value != null && e.ValidationResult != null)
{
IEnumerable<ChangeSetEntry> updateOperations =
ChangeSet.ChangeSetEntries.Where(
p => p.Operation == ChangeOperation.Insert ||
p.Operation == ChangeOperation.Update ||
p.Operation == ChangeOperation.Delete);
ChangeSetEntry operation = updateOperations.SingleOrDefault(p => Object.ReferenceEquals(p.Entity, e.Value));
if (operation != null)
{
ValidationResultInfo error = new ValidationResultInfo(e.ValidationResult.ErrorMessage, e.ValidationResult.MemberNames);
error.StackTrace = e.StackTrace;
operation.ValidationErrors = new List<ValidationResultInfo>() { error };
}
}
else
{
throw;
}
}
return !ChangeSet.HasError;
}
}
}

View File

@@ -0,0 +1,19 @@
// Copyright (c) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information.
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Http.Controllers;
namespace Microsoft.Web.Http.Data
{
public sealed class DataControllerActionInvoker : ApiControllerActionInvoker
{
public override Task<HttpResponseMessage> InvokeActionAsync(HttpActionContext actionContext, CancellationToken cancellationToken)
{
DataController controller = (DataController)actionContext.ControllerContext.Controller;
controller.ActionContext = actionContext;
return base.InvokeActionAsync(actionContext, cancellationToken);
}
}
}

View File

@@ -0,0 +1,36 @@
// Copyright (c) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information.
using System;
using System.Web.Http;
using System.Web.Http.Controllers;
namespace Microsoft.Web.Http.Data
{
public sealed class DataControllerActionSelector : ApiControllerActionSelector
{
private const string ActionRouteKey = "action";
private const string SubmitActionValue = "Submit";
public override HttpActionDescriptor SelectAction(HttpControllerContext controllerContext)
{
// first check to see if this is a call to Submit
string actionName;
if (controllerContext.RouteData.Values.TryGetValue(ActionRouteKey, out actionName) && actionName.Equals(SubmitActionValue, StringComparison.Ordinal))
{
return new SubmitActionDescriptor(controllerContext.ControllerDescriptor, controllerContext.Controller.GetType());
}
// next check to see if this is a direct invocation of a CUD action
DataControllerDescription description = DataControllerDescription.GetDescription(controllerContext.ControllerDescriptor);
UpdateActionDescriptor action = description.GetUpdateAction(actionName);
if (action != null)
{
return new SubmitProxyActionDescriptor(action);
}
// for all other non-CUD operations, we wrap the descriptor in our
// customizing descriptor to layer on additional functionality.
return new CustomizingActionDescriptor(base.SelectAction(controllerContext));
}
}
}

View File

@@ -0,0 +1,107 @@
// Copyright (c) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http.Formatting;
using System.Runtime.Serialization;
using System.Web.Http;
using System.Web.Http.Controllers;
using System.Web.Http.ModelBinding;
using System.Web.Http.Validation;
using Newtonsoft.Json;
namespace Microsoft.Web.Http.Data
{
public class DataControllerActionValueBinder : DefaultActionValueBinder
{
private static ConcurrentDictionary<Type, IEnumerable<SerializerInfo>> _serializerCache = new ConcurrentDictionary<Type, IEnumerable<SerializerInfo>>();
private MediaTypeFormatter[] _formatters;
protected override IEnumerable<MediaTypeFormatter> GetFormatters(HttpActionDescriptor actionDescriptor)
{
if (_formatters == null)
{
HttpControllerDescriptor descr = actionDescriptor.ControllerDescriptor;
HttpConfiguration config = actionDescriptor.Configuration;
DataControllerDescription dataDesc = DataControllerDescription.GetDescription(descr);
List<MediaTypeFormatter> list = new List<MediaTypeFormatter>();
AddFormattersFromConfig(list, config);
AddDataControllerFormatters(list, dataDesc);
_formatters = list.ToArray();
}
return _formatters;
}
protected override IBodyModelValidator GetBodyModelValidator(HttpActionDescriptor actionDescriptor)
{
return null;
}
private static void AddDataControllerFormatters(List<MediaTypeFormatter> formatters, DataControllerDescription description)
{
var cachedSerializers = _serializerCache.GetOrAdd(description.ControllerType, controllerType =>
{
// for the specified controller type, set the serializers for the built
// in framework types
List<SerializerInfo> serializers = new List<SerializerInfo>();
Type[] exposedTypes = description.EntityTypes.ToArray();
serializers.Add(GetSerializerInfo(typeof(ChangeSetEntry[]), exposedTypes));
serializers.Add(GetSerializerInfo(typeof(QueryResult), exposedTypes));
return serializers;
});
JsonMediaTypeFormatter formatterJson = new JsonMediaTypeFormatter();
formatterJson.SerializerSettings = new JsonSerializerSettings() { PreserveReferencesHandling = PreserveReferencesHandling.Objects, TypeNameHandling = TypeNameHandling.All };
XmlMediaTypeFormatter formatterXml = new XmlMediaTypeFormatter();
// apply the serializers to configuration
foreach (var serializerInfo in cachedSerializers)
{
formatterXml.SetSerializer(serializerInfo.ObjectType, serializerInfo.XmlSerializer);
}
formatters.Add(formatterJson);
formatters.Add(formatterXml);
}
// Get existing formatters from config, excluding Json/Xml formatters.
private static void AddFormattersFromConfig(List<MediaTypeFormatter> formatters, HttpConfiguration config)
{
foreach (var formatter in config.Formatters)
{
if (formatter.GetType() == typeof(JsonMediaTypeFormatter) ||
formatter.GetType() == typeof(XmlMediaTypeFormatter))
{
// skip copying the json/xml formatters since we're configuring those
// specifically per controller type and can't share instances between
// controllers
continue;
}
formatters.Add(formatter);
}
}
private static SerializerInfo GetSerializerInfo(Type type, IEnumerable<Type> knownTypes)
{
SerializerInfo info = new SerializerInfo();
info.ObjectType = type;
info.XmlSerializer = new DataContractSerializer(type, knownTypes);
return info;
}
private class SerializerInfo
{
public Type ObjectType { get; set; }
public DataContractSerializer XmlSerializer { get; set; }
}
}
}

View File

@@ -0,0 +1,440 @@
// Copyright (c) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Reflection;
using System.Web.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;
using Microsoft.Web.Http.Data.Metadata;
namespace Microsoft.Web.Http.Data
{
public class DataControllerDescription
{
private static readonly ConcurrentDictionary<Type, DataControllerDescription> _descriptionMap = new ConcurrentDictionary<Type, DataControllerDescription>();
private static ConcurrentDictionary<Type, HashSet<Type>> _typeDescriptionProviderMap = new ConcurrentDictionary<Type, HashSet<Type>>();
private static readonly string[] _deletePrefixes = { "Delete", "Remove" };
private static readonly string[] _insertPrefixes = { "Insert", "Add", "Create" };
private static readonly string[] _updatePrefixes = { "Update", "Change", "Modify" };
private Type _dataControllerType;
private ReadOnlyCollection<Type> _entityTypes;
private List<UpdateActionDescriptor> _updateActions;
internal DataControllerDescription(Type dataControllerType, IEnumerable<Type> entityTypes, List<UpdateActionDescriptor> actions)
{
_dataControllerType = dataControllerType;
_entityTypes = entityTypes.ToList().AsReadOnly();
_updateActions = actions;
}
/// <summary>
/// Gets the Type of the <see cref="DataController"/>
/// </summary>
public Type ControllerType
{
get { return _dataControllerType; }
}
/// <summary>
/// Gets the entity types exposed by the <see cref="DataController"/>
/// </summary>
public IEnumerable<Type> EntityTypes
{
get { return _entityTypes; }
}
public static DataControllerDescription GetDescription(HttpControllerDescriptor controllerDescriptor)
{
return _descriptionMap.GetOrAdd(controllerDescriptor.ControllerType, type =>
{
return CreateDescription(controllerDescriptor);
});
}
/// <summary>
/// Creates and returns the metadata provider for the specified DataController Type.
/// </summary>
/// <param name="dataControllerType">The DataController Type.</param>
/// <returns>The metadata provider.</returns>
internal static MetadataProvider CreateMetadataProvider(Type dataControllerType)
{
// construct a list of all types in the inheritance hierarchy for the controller
List<Type> baseTypes = new List<Type>();
Type currType = dataControllerType;
while (currType != typeof(DataController))
{
baseTypes.Add(currType);
currType = currType.BaseType;
}
// create our base reflection provider
List<MetadataProvider> providerList = new List<MetadataProvider>();
ReflectionMetadataProvider reflectionProvider = new ReflectionMetadataProvider();
// Set the IsEntity function which consults the chain of providers.
Func<Type, bool> isEntityTypeFunc = (t) => providerList.Any(p => p.LookUpIsEntityType(t));
reflectionProvider.SetIsEntityTypeFunc(isEntityTypeFunc);
// Now from most derived to base, create any declared metadata providers,
// chaining the instances as we progress. Note that ordering from derived to
// base is important - we want to ensure that any providers the user has placed on
// their DataController directly come before any DAL providers.
MetadataProvider currProvider = reflectionProvider;
providerList.Add(currProvider);
for (int i = 0; i < baseTypes.Count; i++)
{
currType = baseTypes[i];
// Reflection rather than TD is used here so we only get explicit
// Type attributes. TD inherits attributes by default, even if the
// attributes aren't inheritable.
foreach (MetadataProviderAttribute providerAttribute in
currType.GetCustomAttributes(typeof(MetadataProviderAttribute), false))
{
currProvider = providerAttribute.CreateProvider(dataControllerType, currProvider);
currProvider.SetIsEntityTypeFunc(isEntityTypeFunc);
providerList.Add(currProvider);
}
}
return currProvider;
}
private static DataControllerDescription CreateDescription(HttpControllerDescriptor controllerDescriptor)
{
Type dataControllerType = controllerDescriptor.ControllerType;
MetadataProvider metadataProvider = CreateMetadataProvider(dataControllerType);
// get all public candidate methods and create the operations
HashSet<Type> entityTypes = new HashSet<Type>();
List<UpdateActionDescriptor> actions = new List<UpdateActionDescriptor>();
IEnumerable<MethodInfo> methodsToInspect =
dataControllerType.GetMethods(BindingFlags.Instance | BindingFlags.Public)
.Where(p => (p.DeclaringType != typeof(DataController) && (p.DeclaringType != typeof(object))) && !p.IsSpecialName);
foreach (MethodInfo method in methodsToInspect)
{
if (method.GetCustomAttributes(typeof(NonActionAttribute), false).Length > 0)
{
continue;
}
if (method.IsVirtual && method.GetBaseDefinition().DeclaringType == typeof(DataController))
{
// don't want to infer overrides of DataController virtual methods as
// operations
continue;
}
// We need to ensure the buddy metadata provider is registered BEFORE we
// attempt to do convention, since we rely on IsEntity which relies on
// KeyAttributes being present (possibly from "buddy" classes)
RegisterAssociatedMetadataProvider(method);
ChangeOperation operationType = ClassifyUpdateOperation(method, metadataProvider);
if (operationType != ChangeOperation.None)
{
Type entityType = method.GetParameters()[0].ParameterType;
UpdateActionDescriptor actionDescriptor = new UpdateActionDescriptor(controllerDescriptor, method, entityType, operationType);
ValidateAction(actionDescriptor);
actions.Add(actionDescriptor);
// TODO : currently considering entity types w/o any query methods
// exposing them. Should we?
if (metadataProvider.IsEntityType(entityType))
{
AddEntityType(entityType, entityTypes, metadataProvider);
}
}
else
{
// if the method is a "query" operation returning an entity,
// add to entity types
if (method.ReturnType != typeof(void))
{
Type returnType = TypeUtility.UnwrapTaskInnerType(method.ReturnType);
Type elementType = TypeUtility.GetElementType(returnType);
if (metadataProvider.IsEntityType(elementType))
{
AddEntityType(elementType, entityTypes, metadataProvider);
}
}
}
}
return new DataControllerDescription(dataControllerType, entityTypes, actions);
}
/// <summary>
/// Adds the specified entity type and any associated entity types recursively to the specified set.
/// </summary>
/// <param name="entityType">The entity Type to add.</param>
/// <param name="entityTypes">The types set to accumulate in.</param>
/// <param name="metadataProvider">The metadata provider.</param>
private static void AddEntityType(Type entityType, HashSet<Type> entityTypes, MetadataProvider metadataProvider)
{
if (entityTypes.Contains(entityType))
{
// already added this type
return;
}
entityTypes.Add(entityType);
RegisterDataControllerTypeDescriptionProvider(entityType, metadataProvider);
foreach (PropertyDescriptor pd in TypeDescriptor.GetProperties(entityType))
{
// for any "exposed" association members, recursively add the associated
// entity type
if (pd.Attributes[typeof(AssociationAttribute)] != null && TypeUtility.IsDataMember(pd))
{
Type includedEntityType = TypeUtility.GetElementType(pd.PropertyType);
if (metadataProvider.IsEntityType(entityType))
{
AddEntityType(includedEntityType, entityTypes, metadataProvider);
}
}
}
// Recursively add any derived entity types specified by [KnownType]
// attributes
IEnumerable<Type> knownTypes = TypeUtility.GetKnownTypes(entityType, true);
foreach (Type knownType in knownTypes)
{
if (entityType.IsAssignableFrom(knownType))
{
AddEntityType(knownType, entityTypes, metadataProvider);
}
}
}
private static void ValidateAction(UpdateActionDescriptor updateAction)
{
// Only authorization filters are supported on CUD actions. This will capture 99% of user errors.
// There is the chance that someone might attempt to implement an attribute that implements both
// IAuthorizationFilter AND another filter type, but we don't want to have a black-list of filter
// types here.
if (updateAction.GetFilters().Any(p => !typeof(AuthorizationFilterAttribute).IsAssignableFrom(p.GetType())))
{
throw Error.NotSupported(Resource.InvalidAction_UnsupportedFilterType, updateAction.ControllerDescriptor.ControllerType.Name, updateAction.ActionName);
}
}
private static ChangeOperation ClassifyUpdateOperation(MethodInfo method, MetadataProvider metadataProvider)
{
ChangeOperation operationType;
AttributeCollection methodAttributes = new AttributeCollection(method.GetCustomAttributes(false).Cast<Attribute>().ToArray());
// Check if explicit attributes exist.
if (methodAttributes[typeof(InsertAttribute)] != null)
{
operationType = ChangeOperation.Insert;
}
else if (methodAttributes[typeof(UpdateAttribute)] != null)
{
UpdateAttribute updateAttribute = (UpdateAttribute)methodAttributes[typeof(UpdateAttribute)];
if (updateAttribute.UsingCustomMethod)
{
operationType = ChangeOperation.Custom;
}
else
{
operationType = ChangeOperation.Update;
}
}
else if (methodAttributes[typeof(DeleteAttribute)] != null)
{
operationType = ChangeOperation.Delete;
}
else
{
return TryClassifyUpdateOperationImplicit(method, metadataProvider);
}
return operationType;
}
private static ChangeOperation TryClassifyUpdateOperationImplicit(MethodInfo method, MetadataProvider metadataProvider)
{
ChangeOperation operationType = ChangeOperation.None;
if (method.ReturnType == typeof(void))
{
// Check if this looks like an insert, update or delete method.
if (_insertPrefixes.Any(p => method.Name.StartsWith(p, StringComparison.OrdinalIgnoreCase)))
{
operationType = ChangeOperation.Insert;
}
else if (_updatePrefixes.Any(p => method.Name.StartsWith(p, StringComparison.OrdinalIgnoreCase)))
{
operationType = ChangeOperation.Update;
}
else if (_deletePrefixes.Any(p => method.Name.StartsWith(p, StringComparison.OrdinalIgnoreCase)))
{
operationType = ChangeOperation.Delete;
}
else if (IsCustomUpdateMethod(method, metadataProvider))
{
operationType = ChangeOperation.Custom;
}
}
return operationType;
}
private static bool IsCustomUpdateMethod(MethodInfo method, MetadataProvider metadataProvider)
{
ParameterInfo[] parameters = method.GetParameters();
if (parameters.Length == 0)
{
return false;
}
if (method.ReturnType != typeof(void))
{
return false;
}
return metadataProvider.IsEntityType(parameters[0].ParameterType);
}
/// <summary>
/// Register the associated metadata provider for Types in the signature
/// of the specified method as required.
/// </summary>
/// <param name="methodInfo">The method to register for.</param>
private static void RegisterAssociatedMetadataProvider(MethodInfo methodInfo)
{
Type type = TypeUtility.GetElementType(methodInfo.ReturnType);
if (type != typeof(void) && type.GetCustomAttributes(typeof(MetadataTypeAttribute), true).Length != 0)
{
RegisterAssociatedMetadataTypeTypeDescriptor(type);
}
foreach (ParameterInfo parameter in methodInfo.GetParameters())
{
type = parameter.ParameterType;
if (type != typeof(void) && type.GetCustomAttributes(typeof(MetadataTypeAttribute), true).Length != 0)
{
RegisterAssociatedMetadataTypeTypeDescriptor(type);
}
}
}
/// <summary>
/// Verifies that the <see cref="MetadataTypeAttribute"/> reference does not contain a cyclic reference and
/// registers the AssociatedMetadataTypeTypeDescriptionProvider in that case.
/// </summary>
/// <param name="type">The entity type with the MetadataType attribute.</param>
private static void RegisterAssociatedMetadataTypeTypeDescriptor(Type type)
{
Type currentType = type;
HashSet<Type> metadataTypeReferences = new HashSet<Type>();
metadataTypeReferences.Add(currentType);
while (true)
{
MetadataTypeAttribute attribute = (MetadataTypeAttribute)Attribute.GetCustomAttribute(currentType, typeof(MetadataTypeAttribute));
if (attribute == null)
{
break;
}
else
{
currentType = attribute.MetadataClassType;
// If we find a cyclic reference, throw an error.
if (metadataTypeReferences.Contains(currentType))
{
throw Error.InvalidOperation(Resource.CyclicMetadataTypeAttributesFound, type.FullName);
}
else
{
metadataTypeReferences.Add(currentType);
}
}
}
// If the MetadataType reference chain doesn't contain a cycle, register the use of the AssociatedMetadataTypeTypeDescriptionProvider.
RegisterCustomTypeDescriptor(new AssociatedMetadataTypeTypeDescriptionProvider(type), type);
}
// The JITer enforces CAS. By creating a separate method we can avoid getting SecurityExceptions
// when we weren't going to really call TypeDescriptor.AddProvider.
internal static void RegisterCustomTypeDescriptor(TypeDescriptionProvider tdp, Type type)
{
// Check if we already registered provider with the specified type.
HashSet<Type> existingProviders = _typeDescriptionProviderMap.GetOrAdd(type, t =>
{
return new HashSet<Type>();
});
if (!existingProviders.Contains(tdp.GetType()))
{
TypeDescriptor.AddProviderTransparent(tdp, type);
existingProviders.Add(tdp.GetType());
}
}
/// <summary>
/// Register our DataControllerTypeDescriptionProvider for the specified Type. This provider is responsible for surfacing the
/// custom TDs returned by metadata providers.
/// </summary>
/// <param name="type">The Type that we should register for.</param>
/// <param name="metadataProvider">The metadata provider.</param>
private static void RegisterDataControllerTypeDescriptionProvider(Type type, MetadataProvider metadataProvider)
{
DataControllerTypeDescriptionProvider tdp = new DataControllerTypeDescriptionProvider(type, metadataProvider);
RegisterCustomTypeDescriptor(tdp, type);
}
public UpdateActionDescriptor GetUpdateAction(string name)
{
return _updateActions.FirstOrDefault(p => p.ActionName == name);
}
public UpdateActionDescriptor GetUpdateAction(Type entityType, ChangeOperation operationType)
{
return _updateActions.FirstOrDefault(p => (p.EntityType == entityType) && (p.ChangeOperation == operationType));
}
public UpdateActionDescriptor GetCustomMethod(Type entityType, string methodName)
{
if (entityType == null)
{
throw Error.ArgumentNull("entityType");
}
if (methodName == null)
{
throw Error.ArgumentNull("methodName");
}
return _updateActions.FirstOrDefault(p => (p.EntityType == entityType) && (p.ChangeOperation == ChangeOperation.Custom) && (p.ActionName == methodName));
}
/// <summary>
/// This is the default provider in the metadata provider chain. It is based solely on
/// attributes applied directly to types (either via CLR attributes, or via "buddy" metadata class).
/// </summary>
private class ReflectionMetadataProvider : MetadataProvider
{
public ReflectionMetadataProvider()
: base(parent: null)
{
}
/// <summary>
/// Returns true if the Type has at least one member marked with KeyAttribute.
/// </summary>
/// <param name="type">The Type to check.</param>
/// <returns>True if the Type is an entity, false otherwise.</returns>
public override bool LookUpIsEntityType(Type type)
{
return TypeDescriptor.GetProperties(type).Cast<PropertyDescriptor>().Any(p => p.Attributes[typeof(KeyAttribute)] != null);
}
}
}
}

View File

@@ -0,0 +1,101 @@
// Copyright (c) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Web.Http.Controllers;
using System.Web.Http.Metadata;
using System.Web.Http.ModelBinding;
using System.Web.Http.Validation;
using System.Web.Http.ValueProviders;
namespace Microsoft.Web.Http.Data
{
internal static class DataControllerValidation
{
internal static bool ValidateObject(object o, List<ValidationResultInfo> validationErrors, HttpActionContext actionContext)
{
// create a model validation node for the object
ModelMetadataProvider metadataProvider = actionContext.GetMetadataProvider();
string modelStateKey = String.Empty;
ModelValidationNode validationNode = CreateModelValidationNode(o, metadataProvider, actionContext.ModelState, modelStateKey);
validationNode.ValidateAllProperties = true;
// add the node to model state
ModelState modelState = new ModelState();
modelState.Value = new ValueProviderResult(o, String.Empty, CultureInfo.CurrentCulture);
actionContext.ModelState.Add(modelStateKey, modelState);
// invoke validation
validationNode.Validate(actionContext);
if (!actionContext.ModelState.IsValid)
{
foreach (var modelStateItem in actionContext.ModelState)
{
foreach (ModelError modelError in modelStateItem.Value.Errors)
{
validationErrors.Add(new ValidationResultInfo(modelError.ErrorMessage, new string[] { modelStateItem.Key }));
}
}
}
return actionContext.ModelState.IsValid;
}
private static ModelValidationNode CreateModelValidationNode(object o, ModelMetadataProvider metadataProvider, ModelStateDictionary modelStateDictionary, string modelStateKey)
{
ModelMetadata metadata = metadataProvider.GetMetadataForType(() =>
{
return o;
}, o.GetType());
ModelValidationNode validationNode = new ModelValidationNode(metadata, modelStateKey);
// for this root node, recursively add all child nodes
HashSet<object> visited = new HashSet<object>();
CreateModelValidationNodeRecursive(o, validationNode, metadataProvider, metadata, modelStateDictionary, modelStateKey, visited);
return validationNode;
}
private static void CreateModelValidationNodeRecursive(object o, ModelValidationNode parentNode, ModelMetadataProvider metadataProvider, ModelMetadata metadata, ModelStateDictionary modelStateDictionary, string modelStateKey, HashSet<object> visited)
{
if (visited.Contains(o))
{
return;
}
visited.Add(o);
foreach (PropertyDescriptor property in TypeDescriptor.GetProperties(o))
{
// append the current property name to the model state path
string propertyKey = modelStateKey;
if (propertyKey.Length > 0)
{
propertyKey += ".";
}
propertyKey += property.Name;
// create the node for this property and add to the parent node
object propertyValue = property.GetValue(o);
metadata = metadataProvider.GetMetadataForProperty(() =>
{
return propertyValue;
}, o.GetType(), property.Name);
ModelValidationNode childNode = new ModelValidationNode(metadata, propertyKey);
parentNode.ChildNodes.Add(childNode);
// add the property node to model state
ModelState modelState = new ModelState();
modelState.Value = new ValueProviderResult(propertyValue, null, CultureInfo.CurrentCulture);
modelStateDictionary.Add(propertyKey, modelState);
if (propertyValue != null)
{
CreateModelValidationNodeRecursive(propertyValue, childNode, metadataProvider, metadata, modelStateDictionary, propertyKey, visited);
}
}
}
}
}

View File

@@ -0,0 +1,16 @@
// Copyright (c) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information.
using System;
namespace Microsoft.Web.Http.Data
{
/// <summary>
/// Attribute applied to a <see cref="DataController"/> method to indicate that it is a delete method.
/// </summary>
[AttributeUsage(
AttributeTargets.Field | AttributeTargets.Method | AttributeTargets.Property,
AllowMultiple = false, Inherited = true)]
public sealed class DeleteAttribute : Attribute
{
}
}

View File

@@ -0,0 +1,26 @@
// Copyright (c) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information.
// This file is used by Code Analysis to maintain SuppressMessage
// attributes that are applied to this project.
// Project-level suppressions either have no target or are given
// a specific target and scoped to a namespace, type, member, etc.
//
// To add a suppression to this file, right-click the message in the
// Error List, point to "Suppress Message(s)", and click
// "In Project Suppression File".
// You do not need to add suppressions to this file manually.
using System.Diagnostics.CodeAnalysis;
[assembly: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope = "member", Target = "Microsoft.Web.Http.Data.ChangeSetEntry.#OriginalAssociations")]
[assembly: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope = "member", Target = "Microsoft.Web.Http.Data.ChangeSetEntry.#Associations")]
[assembly: SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Scope = "member", Target = "Microsoft.Web.Http.Data.ChangeSetEntry.#EntityActions")]
[assembly: SuppressMessage("Microsoft.Design", "CA2210:AssembliesShouldHaveValidStrongNames", Justification = "Assembly is delay signed")]
[assembly: SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Scope = "member", Target = "Microsoft.Web.Http.Data.ChangeSet.#SetEntityAssociations()")]
[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "System.ComponentModel.DataAnnotations")]
[assembly: SuppressMessage("Microsoft.Performance", "CA1813:AvoidUnsealedAttributes", Scope = "type", Target = "Microsoft.Web.Http.Data.Metadata.MetadataProviderAttribute", Justification = "We intend for people to derive from this attribute.")]
[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "Microsoft.Web.Http.Data.Metadata", Justification = "These types are in their own namespace to match folder structure.")]
[assembly: SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "LookUp", Scope = "member", Target = "Microsoft.Web.Http.Data.Metadata.MetadataProvider.#LookUpIsEntityType(System.Type)", Justification = "This is intended to read as two words")]
[assembly: SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Scope = "member", Target = "Microsoft.Web.Http.Data.SubmitActionDescriptor.#Execute(System.Web.Http.HttpControllerContext,System.Collections.Generic.IDictionary`2<System.String,System.Object>)", Justification = "This object is being returned - it can't be disposed.")]
[assembly: SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Scope = "member", Target = "Microsoft.Web.Http.Data.DataControllerConfiguration.#CloneConfiguration(System.Web.Http.HttpConfiguration)", Justification = "This object cannot be disposed - it is being set on the execution context")]
[assembly: SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Scope = "member", Target = "Microsoft.Web.Http.Data.QueryFilterAttribute.#GetQueryResult`1(System.Linq.IQueryable`1<!!0>,System.Web.Http.Controllers.HttpActionContext)", Justification = "This object cannot be disposed - it is being returned as the result.")]

View File

@@ -0,0 +1,16 @@
// Copyright (c) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information.
using System;
namespace Microsoft.Web.Http.Data
{
/// <summary>
/// Attribute applied to a <see cref="DataController"/> method to indicate that it is an insert method.
/// </summary>
[AttributeUsage(
AttributeTargets.Field | AttributeTargets.Method | AttributeTargets.Property,
AllowMultiple = false, Inherited = true)]
public sealed class InsertAttribute : Attribute
{
}
}

View File

@@ -0,0 +1,102 @@
// Copyright (c) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Web.Http;
namespace Microsoft.Web.Http.Data.Metadata
{
/// <summary>
/// Custom TypeDescriptionProvider conditionally registered for Types exposed by a <see cref="DataController"/>.
/// </summary>
internal class DataControllerTypeDescriptionProvider : TypeDescriptionProvider
{
private readonly MetadataProvider _metadataProvider;
private readonly Type _type;
private ICustomTypeDescriptor _customTypeDescriptor;
public DataControllerTypeDescriptionProvider(Type type, MetadataProvider metadataProvider)
: base(TypeDescriptor.GetProvider(type))
{
if (metadataProvider == null)
{
throw Error.ArgumentNull("metadataProvider");
}
_type = type;
_metadataProvider = metadataProvider;
}
public override ICustomTypeDescriptor GetTypeDescriptor(Type objectType, object instance)
{
if (objectType == null && instance != null)
{
objectType = instance.GetType();
}
if (_type != objectType)
{
// In inheritance scenarios, we might be called to provide a descriptor
// for a derived Type. In that case, we just return base.
return base.GetTypeDescriptor(objectType, instance);
}
if (_customTypeDescriptor == null)
{
// CLR, buddy class type descriptors
_customTypeDescriptor = base.GetTypeDescriptor(objectType, instance);
// EF, any other custom type descriptors provided through MetadataProviders.
_customTypeDescriptor = _metadataProvider.GetTypeDescriptor(objectType, _customTypeDescriptor);
// initialize FK members AFTER our type descriptors have chained
HashSet<string> foreignKeyMembers = GetForeignKeyMembers();
// if any FK member of any association is also part of the primary key, then the key cannot be marked
// Editable(false)
bool keyIsEditable = false;
foreach (PropertyDescriptor pd in _customTypeDescriptor.GetProperties())
{
if (pd.Attributes[typeof(KeyAttribute)] != null &&
foreignKeyMembers.Contains(pd.Name))
{
keyIsEditable = true;
break;
}
}
if (DataControllerTypeDescriptor.ShouldRegister(_customTypeDescriptor, keyIsEditable, foreignKeyMembers))
{
// Extend the chain with one more descriptor.
_customTypeDescriptor = new DataControllerTypeDescriptor(_customTypeDescriptor, keyIsEditable, foreignKeyMembers);
}
}
return _customTypeDescriptor;
}
/// <summary>
/// Returns the set of all foreign key members for the entity.
/// </summary>
/// <returns>The set of foreign keys.</returns>
private HashSet<string> GetForeignKeyMembers()
{
HashSet<string> foreignKeyMembers = new HashSet<string>();
foreach (PropertyDescriptor pd in _customTypeDescriptor.GetProperties())
{
AssociationAttribute assoc = (AssociationAttribute)pd.Attributes[typeof(AssociationAttribute)];
if (assoc != null && assoc.IsForeignKey)
{
foreach (string foreignKeyMember in assoc.ThisKeyMembers)
{
foreignKeyMembers.Add(foreignKeyMember);
}
}
}
return foreignKeyMembers;
}
}
}

View File

@@ -0,0 +1,247 @@
// Copyright (c) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
namespace Microsoft.Web.Http.Data.Metadata
{
/// <summary>
/// Custom TypeDescriptor for Types exposed by a <see cref="DataController"/>.
/// </summary>
internal class DataControllerTypeDescriptor : CustomTypeDescriptor
{
private readonly HashSet<string> _foreignKeyMembers;
private readonly bool _keyIsEditable;
private PropertyDescriptorCollection _properties;
public DataControllerTypeDescriptor(ICustomTypeDescriptor parent, bool keyIsEditable, HashSet<string> foreignKeyMembers)
: base(parent)
{
_keyIsEditable = keyIsEditable;
_foreignKeyMembers = foreignKeyMembers;
}
public override PropertyDescriptorCollection GetProperties()
{
if (_properties == null)
{
// Get properties from our parent
PropertyDescriptorCollection originalCollection = base.GetProperties();
// Set _properties to avoid a stack overflow when CreateProjectionProperties
// ends up recursively calling TypeDescriptor.GetProperties on a type.
_properties = originalCollection;
bool customDescriptorsCreated = false;
List<PropertyDescriptor> tempPropertyDescriptors = new List<PropertyDescriptor>();
// for every property exposed by our parent, see if we have additional metadata to add,
// and if we do we need to add a wrapper PropertyDescriptor to add the new attributes
foreach (PropertyDescriptor propDescriptor in _properties)
{
Attribute[] newMetadata = GetAdditionalAttributes(propDescriptor);
if (newMetadata.Length > 0)
{
tempPropertyDescriptors.Add(new DataControllerPropertyDescriptor(propDescriptor, newMetadata));
customDescriptorsCreated = true;
}
else
{
tempPropertyDescriptors.Add(propDescriptor);
}
}
if (customDescriptorsCreated)
{
_properties = new PropertyDescriptorCollection(tempPropertyDescriptors.ToArray(), true);
}
}
return _properties;
}
/// <summary>
/// Return an array of new attributes for the specified PropertyDescriptor. If no
/// attributes need to be added, return an empty array.
/// </summary>
/// <param name="pd">The property to add attributes for.</param>
/// <returns>The collection of new attributes.</returns>
private Attribute[] GetAdditionalAttributes(PropertyDescriptor pd)
{
List<Attribute> additionalAttributes = new List<Attribute>();
if (ShouldAddRoundTripAttribute(pd, _foreignKeyMembers.Contains(pd.Name)))
{
additionalAttributes.Add(new RoundtripOriginalAttribute());
}
bool allowInitialValue;
if (ShouldAddEditableFalseAttribute(pd, _keyIsEditable, out allowInitialValue))
{
additionalAttributes.Add(new EditableAttribute(false) { AllowInitialValue = allowInitialValue });
}
return additionalAttributes.ToArray();
}
/// <summary>
/// Determines whether a type uses any features requiring the
/// <see cref="DataControllerTypeDescriptor"/> to be registered. We do this
/// check as an optimization so we're not adding additional TDPs to the
/// chain when they're not necessary.
/// </summary>
/// <param name="descriptor">The descriptor for the type to check.</param>
/// <param name="keyIsEditable">Indicates whether the key for this Type is editable.</param>
/// <param name="foreignKeyMembers">The set of foreign key members for the Type.</param>
/// <returns>Returns <c>true</c> if the type uses any features requiring the
/// <see cref="DataControllerTypeDescriptionProvider"/> to be registered.</returns>
internal static bool ShouldRegister(ICustomTypeDescriptor descriptor, bool keyIsEditable, HashSet<string> foreignKeyMembers)
{
foreach (PropertyDescriptor pd in descriptor.GetProperties())
{
// If there are any attributes that should be inferred for this member, then
// we will register the descriptor
if (ShouldInferAttributes(pd, keyIsEditable, foreignKeyMembers))
{
return true;
}
}
return false;
}
/// <summary>
/// Returns true if the specified member requires a RoundTripOriginalAttribute
/// and one isn't already present.
/// </summary>
/// <param name="pd">The member to check.</param>
/// <param name="isFkMember">True if the member is a foreign key, false otherwise.</param>
/// <returns>True if RoundTripOriginalAttribute should be added, false otherwise.</returns>
private static bool ShouldAddRoundTripAttribute(PropertyDescriptor pd, bool isFkMember)
{
if (pd.Attributes[typeof(RoundtripOriginalAttribute)] != null || pd.Attributes[typeof(AssociationAttribute)] != null)
{
// already has the attribute or is an association
return false;
}
if (isFkMember || pd.Attributes[typeof(ConcurrencyCheckAttribute)] != null ||
pd.Attributes[typeof(TimestampAttribute)] != null || pd.Attributes[typeof(KeyAttribute)] != null)
{
return true;
}
return false;
}
/// <summary>
/// Returns <c>true</c> if the specified member requires an <see cref="EditableAttribute"/>
/// to make the member read-only and one isn't already present.
/// </summary>
/// <param name="pd">The member to check.</param>
/// <param name="keyIsEditable">Indicates whether the key for this Type is editable.</param>
/// <param name="allowInitialValue">
/// The default that should be used for <see cref="EditableAttribute.AllowInitialValue"/> if the attribute
/// should be added to the member.
/// </param>
/// <returns><c>true</c> if <see cref="EditableAttribute"/> should be added, <c>false</c> otherwise.</returns>
private static bool ShouldAddEditableFalseAttribute(PropertyDescriptor pd, bool keyIsEditable, out bool allowInitialValue)
{
allowInitialValue = false;
if (pd.Attributes[typeof(EditableAttribute)] != null)
{
// already has the attribute
return false;
}
bool hasKeyAttribute = (pd.Attributes[typeof(KeyAttribute)] != null);
if (hasKeyAttribute && keyIsEditable)
{
return false;
}
if (hasKeyAttribute || pd.Attributes[typeof(TimestampAttribute)] != null)
{
// If we're inferring EditableAttribute because of a KeyAttribute
// we want to allow initial value for the member.
allowInitialValue = hasKeyAttribute;
return true;
}
return false;
}
/// <summary>
/// Determines if there are any attributes that can be inferred for the specified member.
/// </summary>
/// <param name="pd">The member to check.</param>
/// <param name="keyIsEditable">Indicates whether the key for this Type is editable.</param>
/// <param name="foreignKeyMembers">Collection of foreign key members for the Type.</param>
/// <returns><c>true</c> if there are attributes to be inferred, <c>false</c> otherwise.</returns>
private static bool ShouldInferAttributes(PropertyDescriptor pd, bool keyIsEditable, IEnumerable<string> foreignKeyMembers)
{
bool allowInitialValue;
return ShouldAddEditableFalseAttribute(pd, keyIsEditable, out allowInitialValue) ||
ShouldAddRoundTripAttribute(pd, foreignKeyMembers.Contains(pd.Name));
}
}
/// <summary>
/// PropertyDescriptor wrapper.
/// </summary>
internal class DataControllerPropertyDescriptor : PropertyDescriptor
{
private PropertyDescriptor _base;
public DataControllerPropertyDescriptor(PropertyDescriptor pd, Attribute[] attribs)
: base(pd, attribs)
{
_base = pd;
}
public override Type ComponentType
{
get { return _base.ComponentType; }
}
public override bool IsReadOnly
{
get { return _base.IsReadOnly; }
}
public override Type PropertyType
{
get { return _base.PropertyType; }
}
public override object GetValue(object component)
{
return _base.GetValue(component);
}
public override void SetValue(object component, object value)
{
_base.SetValue(component, value);
}
public override bool ShouldSerializeValue(object component)
{
return _base.ShouldSerializeValue(component);
}
public override bool CanResetValue(object component)
{
return _base.CanResetValue(component);
}
public override void ResetValue(object component)
{
_base.ResetValue(component);
}
}
}

View File

@@ -0,0 +1,113 @@
// Copyright (c) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information.
using System;
using System.ComponentModel;
using System.Web.Http;
namespace Microsoft.Web.Http.Data.Metadata
{
/// <summary>
/// A <see cref="MetadataProvider"/> is used to provide the metadata description for
/// types exposed by a <see cref="DataController"/>.
/// </summary>
public abstract class MetadataProvider
{
private MetadataProvider _parentProvider;
private Func<Type, bool> _isEntityTypeFunc;
/// <summary>
/// Protected Constructor
/// </summary>
/// <param name="parent">The existing parent provider. May be null.</param>
protected MetadataProvider(MetadataProvider parent)
{
_parentProvider = parent;
}
/// <summary>
/// Gets the parent provider.
/// </summary>
internal MetadataProvider ParentProvider
{
get { return _parentProvider; }
}
/// <summary>
/// Gets the <see cref="TypeDescriptor"/> for the specified Type, using the specified parent descriptor
/// as the base. Overrides should call base to ensure the <see cref="TypeDescriptor"/>s are chained properly.
/// </summary>
/// <param name="type">The Type to return a descriptor for.</param>
/// <param name="parent">The parent descriptor.</param>
/// <returns>The <see cref="TypeDescriptor"/> for the specified Type.</returns>
public virtual ICustomTypeDescriptor GetTypeDescriptor(Type type, ICustomTypeDescriptor parent)
{
if (type == null)
{
throw Error.ArgumentNull("type");
}
if (parent == null)
{
throw Error.ArgumentNull("parent");
}
if (_parentProvider != null)
{
return _parentProvider.GetTypeDescriptor(type, parent);
}
return parent;
}
/// <summary>
/// Determines if the specified <see cref="Type"/> should be considered an entity <see cref="Type"/>.
/// The base implementation returns <c>false</c>.
/// </summary>
/// <remarks>Effectively, the return from this method is this provider's vote as to whether the specified
/// Type is an entity. The votes from this provider and all other providers in the chain are used
/// by <see cref="IsEntityType"/> to make it's determination.</remarks>
/// <param name="type">The <see cref="Type"/> to check.</param>
/// <returns>Returns <c>true</c> if the <see cref="Type"/> should be considered an entity,
/// <c>false</c> otherwise.</returns>
public virtual bool LookUpIsEntityType(Type type)
{
if (type == null)
{
throw Error.ArgumentNull("type");
}
return false;
}
/// <summary>
/// Determines if the specified <see cref="Type"/> is an entity <see cref="Type"/> by consulting
/// the <see cref="LookUpIsEntityType"/> method of all <see cref="MetadataProvider"/>s
/// in the provider chain for the <see cref="DataController"/>.
/// </summary>
/// <param name="type">The <see cref="Type"/> to check.</param>
/// <returns>Returns <c>true</c> if the <see cref="Type"/> is an entity, <c>false</c> otherwise.</returns>
protected internal bool IsEntityType(Type type)
{
if (type == null)
{
throw Error.ArgumentNull("type");
}
if (_isEntityTypeFunc != null)
{
return _isEntityTypeFunc(type);
}
return false;
}
/// <summary>
/// Sets the internal entity lookup function for this provider. The function consults
/// the entire provider chain to make its determination.
/// </summary>
/// <param name="isEntityTypeFunc">The entity function.</param>
internal void SetIsEntityTypeFunc(Func<Type, bool> isEntityTypeFunc)
{
_isEntityTypeFunc = isEntityTypeFunc;
}
}
}

View File

@@ -0,0 +1,80 @@
// Copyright (c) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information.
using System;
using System.Web.Http;
namespace Microsoft.Web.Http.Data.Metadata
{
/// <summary>
/// Attribute applied to a <see cref="DataController"/> type to specify the <see cref="MetadataProvider"/>
/// for the type.
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
public class MetadataProviderAttribute : Attribute
{
private Type _providerType;
/// <summary>
/// Initializes a new instance of the MetadataProviderAttribute class
/// </summary>
/// <param name="providerType">The <see cref="MetadataProvider"/> type</param>
public MetadataProviderAttribute(Type providerType)
{
if (providerType == null)
{
throw Error.ArgumentNull("providerType");
}
_providerType = providerType;
}
/// <summary>
/// Gets the <see cref="MetadataProvider"/> type
/// </summary>
public Type ProviderType
{
get { return _providerType; }
}
/// <summary>
/// Gets a unique identifier for this attribute.
/// </summary>
public override object TypeId
{
get { return this; }
}
/// <summary>
/// This method creates an instance of the <see cref="MetadataProvider"/>. Subclasses can override this
/// method to provide their own construction logic.
/// </summary>
/// <param name="controllerType">The <see cref="DataController"/> type to create a metadata provider for.</param>
/// <param name="parent">The parent provider. May be null.</param>
/// <returns>The metadata provider</returns>
public virtual MetadataProvider CreateProvider(Type controllerType, MetadataProvider parent)
{
if (controllerType == null)
{
throw Error.ArgumentNull("controllerType");
}
if (!typeof(DataController).IsAssignableFrom(controllerType))
{
throw Error.Argument("controllerType", Resource.InvalidType, controllerType.FullName, typeof(DataController).FullName);
}
if (!typeof(MetadataProvider).IsAssignableFrom(_providerType))
{
throw Error.InvalidOperation(Resource.InvalidType, _providerType.FullName, typeof(MetadataProvider).FullName);
}
// Verify the type has a .ctor(MetadataProvider).
if (_providerType.GetConstructor(new Type[] { typeof(MetadataProvider) }) == null)
{
throw Error.InvalidOperation(Resource.MetadataProviderAttribute_MissingConstructor, _providerType.FullName);
}
return (MetadataProvider)Activator.CreateInstance(_providerType, parent);
}
}
}

View File

@@ -0,0 +1,155 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory),Runtime.sln))\tools\WebStack.settings.targets" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<CodeAnalysis Condition=" '$(CodeAnalysis)' == '' ">false</CodeAnalysis>
<ProductVersion>8.0.30703</ProductVersion>
<SchemaVersion>2.0</SchemaVersion>
<ProjectGuid>{ACE91549-D86E-4EB6-8C2A-5FF51386BB68}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Microsoft.Web.Http.Data</RootNamespace>
<AssemblyName>Microsoft.Web.Http.Data</AssemblyName>
<FileAlignment>512</FileAlignment>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>..\..\bin\Debug\</OutputPath>
<DefineConstants>TRACE;DEBUG;ASPNETMVC</DefineConstants>
<CodeAnalysisRuleSet>..\Strict.ruleset</CodeAnalysisRuleSet>
<DocumentationFile>$(OutputPath)\$(AssemblyName).xml</DocumentationFile>
<NoWarn>1591</NoWarn>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>..\..\bin\Release\</OutputPath>
<DefineConstants>TRACE;ASPNETMVC</DefineConstants>
<CodeAnalysisRuleSet>..\Strict.ruleset</CodeAnalysisRuleSet>
<RunCodeAnalysis>$(CodeAnalysis)</RunCodeAnalysis>
<DocumentationFile>$(OutputPath)\$(AssemblyName).xml</DocumentationFile>
<NoWarn>1591</NoWarn>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'CodeCoverage|AnyCPU'">
<DebugSymbols>true</DebugSymbols>
<OutputPath>..\..\bin\CodeCoverage\</OutputPath>
<DefineConstants>TRACE;DEBUG;CODE_COVERAGE;ASPNETMVC</DefineConstants>
<DebugType>full</DebugType>
<CodeAnalysisRuleSet>..\Strict.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
<Reference Include="Newtonsoft.Json, Version=4.5.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\..\packages\Newtonsoft.Json.4.5.1\lib\net40\Newtonsoft.Json.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.ComponentModel.DataAnnotations" />
<Reference Include="System.Core" />
<Reference Include="System.Net.Http, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\..\packages\Microsoft.Net.Http.2.0.20326.1\lib\net40\System.Net.Http.dll</HintPath>
</Reference>
<Reference Include="System.Net.Http.WebRequest, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\..\packages\Microsoft.Net.Http.2.0.20326.1\lib\net40\System.Net.Http.WebRequest.dll</HintPath>
</Reference>
<Reference Include="System.Runtime.Serialization" />
<Reference Include="System.XML" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\AptcaCommonAssemblyInfo.cs">
<Link>Properties\AptcaCommonAssemblyInfo.cs</Link>
</Compile>
<Compile Include="..\CommonAssemblyInfo.cs">
<Link>Properties\CommonAssemblyInfo.cs</Link>
</Compile>
<Compile Include="..\Common\DictionaryExtensions.cs">
<Link>Common\DictionaryExtensions.cs</Link>
</Compile>
<Compile Include="..\Common\Error.cs">
<Link>Common\Error.cs</Link>
</Compile>
<Compile Include="..\Common\TaskHelpers.cs">
<Link>Common\TaskHelpers.cs</Link>
</Compile>
<Compile Include="..\Common\TaskHelpersExtensions.cs">
<Link>Common\TaskHelpersExtensions.cs</Link>
</Compile>
<Compile Include="ChangeOperation.cs" />
<Compile Include="ChangeSet.cs" />
<Compile Include="ChangeSetEntry.cs" />
<Compile Include="DataControllerActionInvoker.cs" />
<Compile Include="CustomizingActionDescriptor.cs" />
<Compile Include="DataController.cs" />
<Compile Include="DataControllerActionSelector.cs" />
<Compile Include="DataControllerActionValueBinder.cs" />
<Compile Include="DataControllerDescription.cs" />
<Compile Include="DataControllerValidation.cs" />
<Compile Include="Metadata\DataControllerTypeDescriptor.cs" />
<Compile Include="Metadata\DataControllerTypeDescriptionProvider.cs" />
<Compile Include="DeleteAttribute.cs" />
<Compile Include="GlobalSuppressions.cs" />
<Compile Include="InsertAttribute.cs" />
<Compile Include="Metadata\MetadataProvider.cs" />
<Compile Include="Metadata\MetadataProviderAttribute.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="QueryFilterAttribute.cs" />
<Compile Include="QueryResult.cs" />
<Compile Include="Resource.Designer.cs">
<AutoGen>True</AutoGen>
<DesignTime>True</DesignTime>
<DependentUpon>Resource.resx</DependentUpon>
</Compile>
<Compile Include="RoundtripOriginalAttribute.cs" />
<Compile Include="SubmitActionDescriptor.cs" />
<Compile Include="SubmitProxyActionDescriptor.cs" />
<Compile Include="TypeDescriptorExtensions.cs" />
<Compile Include="TypeUtility.cs" />
<Compile Include="UpdateActionDescriptor.cs" />
<Compile Include="UpdateAttribute.cs" />
<Compile Include="ValidationResultInfo.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\System.Net.Http.Formatting\System.Net.Http.Formatting.csproj">
<Project>{668E9021-CE84-49D9-98FB-DF125A9FCDB0}</Project>
<Name>System.Net.Http.Formatting</Name>
</ProjectReference>
<ProjectReference Include="..\System.Web.Http\System.Web.Http.csproj">
<Project>{DDC1CE0C-486E-4E35-BB3B-EAB61F8F9440}</Project>
<Name>System.Web.Http</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<CodeAnalysisDictionary Include="..\CodeAnalysisDictionary.xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\Common\CommonWebApiResources.Designer.cs">
<Link>Properties\CommonWebApiResources.Designer.cs</Link>
<AutoGen>True</AutoGen>
<DesignTime>True</DesignTime>
<DependentUpon>CommonWebApiResources.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="..\Common\CommonWebApiResources.resx">
<Link>Properties\CommonWebApiResources.resx</Link>
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>CommonWebApiResources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Resource.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resource.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

View File

@@ -0,0 +1,8 @@
// Copyright (c) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information.
using System.Reflection;
using System.Runtime.CompilerServices;
[assembly: AssemblyTitle("Microsoft.Web.Http.Data")]
[assembly: AssemblyDescription("Microsoft.Web.Http.Data")]
[assembly: InternalsVisibleTo("Microsoft.Web.Http.Data.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")]

View File

@@ -0,0 +1,136 @@
// Copyright (c) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information.
using System;
using System.Linq;
using System.Linq.Expressions;
using System.Net.Http;
using System.Reflection;
using System.Web.Http;
using System.Web.Http.Filters;
namespace Microsoft.Web.Http.Data
{
[AttributeUsage(AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
internal sealed class QueryFilterAttribute : QueryableAttribute
{
internal static readonly string TotalCountKey = "MS_InlineCountKey";
private static readonly MethodInfo _getTotalCountMethod = typeof(QueryFilterAttribute).GetMethod("GetTotalCount", BindingFlags.NonPublic | BindingFlags.Instance);
protected override IQueryable ApplyResultLimit(HttpActionExecutedContext actionExecutedContext, IQueryable query)
{
if (actionExecutedContext == null)
{
throw Error.ArgumentNull("actionExecutedContext");
}
if (query == null)
{
throw Error.ArgumentNull("query");
}
HttpRequestMessage request = actionExecutedContext.Request;
bool inlineCount = ShouldInlineCount(request);
HttpResponseMessage response = actionExecutedContext.Response;
if (response != null && inlineCount && query != null)
{
// Compute the total count and add the result as a request property. Later after all
// filters have run, DataController will transform the final result into a QueryResult
// which includes this value.
// TODO : use a compiled/cached delegate?
int totalCount = (int)_getTotalCountMethod.MakeGenericMethod(query.ElementType).Invoke(this, new object[] { query });
request.Properties.Add(QueryFilterAttribute.TotalCountKey, totalCount);
}
return base.ApplyResultLimit(actionExecutedContext, query);
}
private static bool ShouldInlineCount(HttpRequestMessage request)
{
if (request != null && request.RequestUri != null && !String.IsNullOrWhiteSpace(request.RequestUri.Query))
{
// search the URI for an inline count request
var parsedQuery = request.RequestUri.ParseQueryString();
var inlineCountPart = parsedQuery["$inlinecount"];
if (inlineCountPart == "allpages")
{
return true;
}
}
return false;
}
/// <summary>
/// Determine the total count for the specified query.
/// </summary>
private int GetTotalCount<T>(IQueryable<T> results)
{
// A total count of -1 indicates that the count is the result count. This
// is the default, unless we discover that skip/top operations will be
// performed, in which case we'll form and execute a count query.
int totalCount = -1;
IQueryable totalCountQuery = null;
if (TryRemovePaging(results, out totalCountQuery))
{
totalCount = ((IQueryable<T>)totalCountQuery).Count();
}
else if (ResultLimit > 0)
{
// The client query didn't specify any skip/top paging operations.
// However, this action has a ResultLimit applied.
// Therefore, we need to take the count now before that limit is applied.
totalCount = results.Count();
}
return totalCount;
}
/// <summary>
/// Inspects the specified query and if the query has any paging operators
/// at the end of it (either a single Take or a Skip/Take) the underlying
/// query w/o the Skip/Take is returned.
/// </summary>
/// <param name="query">The query to inspect.</param>
/// <param name="countQuery">The resulting count query. Null if there is no paging.</param>
/// <returns>True if a count query is returned, false otherwise.</returns>
internal static bool TryRemovePaging(IQueryable query, out IQueryable countQuery)
{
MethodCallExpression mce = query.Expression as MethodCallExpression;
Expression countExpr = null;
// TODO what if the paging does not follow the exact Skip().Take() pattern?
if (IsSequenceOperator("take", mce))
{
// strip off the Take operator
countExpr = mce.Arguments[0];
mce = countExpr as MethodCallExpression;
if (IsSequenceOperator("skip", mce))
{
// If there's a skip then we need to exclude that too. No skip means we're
// on the first page.
countExpr = mce.Arguments[0];
}
}
countQuery = null;
if (countExpr != null)
{
countQuery = query.Provider.CreateQuery(countExpr);
return true;
}
return false;
}
private static bool IsSequenceOperator(string operatorName, MethodCallExpression mce)
{
if (mce != null && mce.Method.DeclaringType == typeof(Queryable) &&
mce.Method.Name.Equals(operatorName, StringComparison.OrdinalIgnoreCase))
{
return true;
}
return false;
}
}
}

Some files were not shown because too many files have changed in this diff Show More