// 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;
///
/// Gets the current . Returns null if no change operations are being performed.
///
protected ChangeSet ChangeSet
{
get { return _changeSet; }
}
///
/// Gets the for this .
///
protected DataControllerDescription Description
{
get { return _description; }
}
///
/// Gets the for the currently executing action.
///
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);
}
///
/// Performs the operations indicated by the specified by invoking
/// the corresponding actions for each.
///
/// The changeset to submit
/// True if the submit was successful, false otherwise.
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 ExecuteAsync(HttpControllerContext controllerContext, CancellationToken cancellationToken)
{
return base.ExecuteAsync(controllerContext, cancellationToken)
.Then(response =>
{
int totalCount;
if (response != null &&
controllerContext.Request.Properties.TryGetValue(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;
});
}
///
/// For all operations in the current changeset, validate that the operation exists, and
/// set the operation entry.
///
internal static void ResolveActions(DataControllerDescription description, IEnumerable 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;
}
}
///
/// Verifies the user is authorized to submit the current .
///
/// True if the is authorized, false otherwise.
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;
}
///
/// Validates the current . Any errors should be set on the individual s
/// in the .
///
/// True if all operations in the passed validation, false otherwise.
protected virtual bool ValidateChangeSet()
{
return ChangeSet.Validate(ActionContext);
}
///
/// This method invokes the action for each operation in the current .
///
/// True if the was processed successfully, false otherwise.
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