PageRenderTime 43ms CodeModel.GetById 14ms RepoModel.GetById 1ms app.codeStats 0ms

/retry/src/Aftermath.WebApi/DataController.cs

http://aftermathjs.codeplex.com
C# | 263 lines | 165 code | 34 blank | 64 comment | 43 complexity | bccbf0ce11deac9a8b2bed9b3a0fdf18 MD5 | raw file
  1. using System;
  2. using System.Collections.Generic;
  3. using System.ComponentModel.DataAnnotations;
  4. using System.Linq;
  5. using System.Reflection;
  6. using System.Threading;
  7. using System.Web.Http;
  8. using System.Web.Http.Controllers;
  9. namespace Aftermath {
  10. [DataControllerConfiguration]
  11. public class DataController : ApiController {
  12. /// <summary>
  13. /// Gets the current <see cref="ChangeSet" />. Returns null if no change operations are being performed.
  14. /// </summary>
  15. protected ChangeSet ChangeSet { get; private set; }
  16. /// <summary>
  17. /// Gets the <see cref="DataControllerDescription" /> for this <see cref="DataController" />.
  18. /// </summary>
  19. protected DataControllerDescription Description { get; private set; }
  20. /// <summary>
  21. /// Gets the <see cref="System.Web.Http.Controllers.HttpActionContext" /> for the currently executing action.
  22. /// </summary>
  23. protected internal HttpActionContext ActionContext { get; internal set; }
  24. protected override void Initialize(HttpControllerContext controllerContext) {
  25. // ensure that the service is valid and all custom metadata providers
  26. // have been registered (don't worry about performance here, there's a coalesce with a static var underneath)
  27. Description = DataControllerDescription.GetDescription(controllerContext.ControllerDescriptor);
  28. base.Initialize(controllerContext);
  29. }
  30. /// <summary>
  31. /// Performs the operations indicated by the specified <see cref="ChangeSet" /> by invoking
  32. /// the corresponding actions for each.
  33. /// </summary>
  34. /// <param name="changeSet"> The changeset to submit </param>
  35. /// <returns> True if the submit was successful, false otherwise. </returns>
  36. public virtual bool Submit(ChangeSet changeSet) {
  37. if (changeSet == null)
  38. throw Error.ArgumentNull("changeSet");
  39. ChangeSet = changeSet;
  40. ResolveActions(Description, ChangeSet.ChangeSetEntries);
  41. if (!AuthorizeChangeSet()) {
  42. // Don't try to save if there were any errors.
  43. return false;
  44. }
  45. // Before invoking any operations, validate the entire changeset
  46. if (!ValidateChangeSet())
  47. return false;
  48. // Now that we're validated, proceed to invoke the actions.
  49. if (!ExecuteChangeSet())
  50. return false;
  51. // persist the changes
  52. if (!PersistChangeSetInternal())
  53. return false;
  54. return true;
  55. }
  56. /// <summary>
  57. /// For all operations in the current changeset, validate that the operation exists, and
  58. /// set the operation entry.
  59. /// </summary>
  60. internal static void ResolveActions(DataControllerDescription description, IEnumerable<ChangeSetEntry> changeSet) {
  61. // Resolve and set the action for each operation in the changeset
  62. foreach (var changeSetEntry in changeSet) {
  63. var entityType = changeSetEntry.Entity.GetType();
  64. UpdateActionDescriptor actionDescriptor = null;
  65. if (changeSetEntry.Operation == ChangeOperation.Insert ||
  66. changeSetEntry.Operation == ChangeOperation.Update ||
  67. changeSetEntry.Operation == ChangeOperation.Delete)
  68. actionDescriptor = description.GetUpdateAction(entityType, changeSetEntry.Operation);
  69. // if a custom method invocation is specified, validate that the method exists
  70. var isCustomUpdate = false;
  71. if (changeSetEntry.EntityActions != null && changeSetEntry.EntityActions.Any()) {
  72. var entityAction = changeSetEntry.EntityActions.Single();
  73. var customMethodOperation = description.GetCustomMethod(entityType, entityAction.Key);
  74. if (customMethodOperation == null) {
  75. throw Error.InvalidOperation(
  76. Resource.DataController_InvalidAction,
  77. entityAction.Key,
  78. entityType.Name);
  79. }
  80. // if the primary action for an update is null but the entry
  81. // contains a valid custom update action, its considered a "custom update"
  82. isCustomUpdate = actionDescriptor == null && customMethodOperation != null;
  83. }
  84. if (actionDescriptor == null && !isCustomUpdate) {
  85. throw Error.InvalidOperation(
  86. Resource.DataController_InvalidAction,
  87. changeSetEntry.Operation.ToString(),
  88. entityType.Name);
  89. }
  90. changeSetEntry.ActionDescriptor = actionDescriptor;
  91. }
  92. }
  93. /// <summary>
  94. /// Verifies the user is authorized to submit the current <see cref="ChangeSet" />.
  95. /// </summary>
  96. /// <returns> True if the <see cref="ChangeSet" /> is authorized, false otherwise. </returns>
  97. protected virtual bool AuthorizeChangeSet() {
  98. foreach (var changeSetEntry in ChangeSet.ChangeSetEntries) {
  99. if (!changeSetEntry.ActionDescriptor.Authorize(ActionContext))
  100. return false;
  101. // if there are any custom method invocations for this operation
  102. // we need to authorize them as well
  103. if (changeSetEntry.EntityActions == null || !changeSetEntry.EntityActions.Any())
  104. continue;
  105. var entityType = changeSetEntry.Entity.GetType();
  106. if (changeSetEntry.EntityActions.Select(entityAction => Description.GetCustomMethod(entityType, entityAction.Key)).Any(customAction => !customAction.Authorize(ActionContext)))
  107. return false;
  108. }
  109. return !ChangeSet.HasError;
  110. }
  111. /// <summary>
  112. /// Validates the current <see cref="ChangeSet" />. Any errors should be set on the individual <see cref="ChangeSetEntry" />s
  113. /// in the <see cref="ChangeSet" />.
  114. /// </summary>
  115. /// <returns> <c>True</c> if all operations in the <see cref="ChangeSet" /> passed validation, <c>false</c> otherwise. </returns>
  116. protected virtual bool ValidateChangeSet() {
  117. return ChangeSet.Validate(ActionContext);
  118. }
  119. /// <summary>
  120. /// This method invokes the action for each operation in the current <see cref="ChangeSet" />.
  121. /// </summary>
  122. /// <returns> True if the <see cref="ChangeSet" /> was processed successfully, false otherwise. </returns>
  123. protected virtual bool ExecuteChangeSet() {
  124. InvokeCUDOperations();
  125. InvokeCustomUpdateOperations();
  126. return !ChangeSet.HasError;
  127. }
  128. private void InvokeCUDOperations() {
  129. foreach (var changeSetEntry in ChangeSet.ChangeSetEntries
  130. .Where(
  131. op => op.Operation == ChangeOperation.Insert ||
  132. op.Operation == ChangeOperation.Update ||
  133. op.Operation == ChangeOperation.Delete)
  134. .Where(changeSetEntry => changeSetEntry.ActionDescriptor != null)) {
  135. InvokeAction(
  136. changeSetEntry.ActionDescriptor,
  137. new[] {
  138. changeSetEntry.Entity
  139. },
  140. changeSetEntry);
  141. }
  142. }
  143. private void InvokeCustomUpdateOperations() {
  144. foreach (
  145. var changeSetEntry in
  146. ChangeSet.ChangeSetEntries.Where(op => op.EntityActions != null && op.EntityActions.Any())) {
  147. var entityType = changeSetEntry.Entity.GetType();
  148. foreach (var entityAction in changeSetEntry.EntityActions) {
  149. var customUpdateAction = Description.GetCustomMethod(entityType, entityAction.Key);
  150. var customMethodParams = new List<object>(entityAction.Value);
  151. customMethodParams.Insert(0, changeSetEntry.Entity);
  152. InvokeAction(customUpdateAction, customMethodParams.ToArray(), changeSetEntry);
  153. }
  154. }
  155. }
  156. private void InvokeAction(HttpActionDescriptor action, IList<object> parameters, ChangeSetEntry changeSetEntry) {
  157. try {
  158. var pds = action.GetParameters();
  159. var paramMap = new Dictionary<string, object>(pds.Count);
  160. for (var i = 0; i < pds.Count; i++)
  161. paramMap.Add(pds[i].ParameterName, parameters[i]);
  162. // TODO - Issue #103
  163. // This method is not correctly observing the execution results, the catch block below is wrong.
  164. // Submit should be Task<bool>, not bool, and should model bind for the CancellationToken which would then
  165. // be propagated through to all the helper methods (one or more of which might also need to be made async,
  166. // once we start respecting the fact that the read/write actions should be allowed to be async).
  167. action.ExecuteAsync(ActionContext.ControllerContext, paramMap, CancellationToken.None);
  168. }
  169. catch (TargetInvocationException tie) {
  170. var vex = tie.GetBaseException() as ValidationException;
  171. if (vex == null)
  172. throw;
  173. var error = new ValidationResultInfo(vex.Message, 0, String.Empty, vex.ValidationResult.MemberNames);
  174. changeSetEntry.ValidationErrors = changeSetEntry.ValidationErrors != null
  175. ? changeSetEntry.ValidationErrors.Concat(
  176. new[] {
  177. error
  178. }).ToArray()
  179. : new[] {
  180. error
  181. };
  182. }
  183. }
  184. /// <summary>
  185. /// This method is called to finalize changes after all the operations in the current <see cref="ChangeSet" />
  186. /// have been invoked. This method should commit the changes as necessary to the data store.
  187. /// Any errors should be set on the individual <see cref="ChangeSetEntry" />s in the <see cref="ChangeSet" />.
  188. /// </summary>
  189. /// <returns> True if the <see cref="ChangeSet" /> was persisted successfully, false otherwise. </returns>
  190. protected virtual bool PersistChangeSet() {
  191. return true;
  192. }
  193. /// <summary>
  194. /// This method invokes the user overridable <see cref="PersistChangeSet" /> method wrapping the call
  195. /// with the appropriate exception handling logic. All framework calls to <see cref="PersistChangeSet" />
  196. /// must go through this method. Some data sources have their own validation hook points,
  197. /// so if a <see cref="System.ComponentModel.DataAnnotations.ValidationException" /> is thrown at that level, we want to capture it.
  198. /// </summary>
  199. /// <returns> True if the <see cref="ChangeSet" /> was persisted successfully, false otherwise. </returns>
  200. private bool PersistChangeSetInternal() {
  201. try {
  202. PersistChangeSet();
  203. }
  204. catch (ValidationException e) {
  205. // if a validation exception is thrown for one of the entities in the changeset
  206. // set the error on the corresponding ChangeSetEntry
  207. if (e.Value == null)
  208. throw;
  209. var updateOperations =
  210. ChangeSet.ChangeSetEntries.Where(
  211. p => p.Operation == ChangeOperation.Insert ||
  212. p.Operation == ChangeOperation.Update ||
  213. p.Operation == ChangeOperation.Delete);
  214. var operation = updateOperations.SingleOrDefault(p => ReferenceEquals(p.Entity, e.Value));
  215. if (operation != null) {
  216. var error = new ValidationResultInfo(
  217. e.ValidationResult.ErrorMessage,
  218. e.ValidationResult.MemberNames) {
  219. StackTrace = e.StackTrace
  220. };
  221. operation.ValidationErrors = new List<ValidationResultInfo> {
  222. error
  223. };
  224. }
  225. }
  226. return !ChangeSet.HasError;
  227. }
  228. }
  229. }