// 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 customMethodParams = new List(entityAction.Value); customMethodParams.Insert(0, changeSetEntry.Entity); InvokeAction(customUpdateAction, customMethodParams.ToArray(), changeSetEntry); } } } private void InvokeAction(HttpActionDescriptor action, object[] parameters, ChangeSetEntry changeSetEntry) { try { Collection pds = action.GetParameters(); Dictionary paramMap = new Dictionary(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; } } } /// /// This method is called to finalize changes after all the operations in the current /// have been invoked. This method should commit the changes as necessary to the data store. /// Any errors should be set on the individual s in the . /// /// True if the was persisted successfully, false otherwise. protected virtual bool PersistChangeSet() { return true; } /// /// This method invokes the user overridable method wrapping the call /// with the appropriate exception handling logic. All framework calls to /// must go through this method. Some data sources have their own validation hook points, /// so if a is thrown at that level, we want to capture it. /// /// True if the was persisted successfully, false otherwise. 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 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() { error }; } } else { throw; } } return !ChangeSet.HasError; } } }