Extending PATCH Support For ASP.NET WebAPI : Part III Validation

In the first two parts of our series we described the general PATCH approach and delved into the details of our ModelBinder. In the final entry, we are going to show how we leveraged our AbstractPatchStateRequest state to understand what got sent to us in a request. In this post, we will leverage that information to trigger validation based on the binding information.

At Ritter Insurance Marketing, we utilize FluentValidation to ensure the validity of our requests. In our opinion, it is the best way to do ASP.NET validation and we highly recommend it. FluentValidation is based on setting rules and then executing each rule over the current model. Let’s recall our example validator.

We are inheriting from AbstractPatchValidator which affords us access to the WhenBound structure. WhenBound will only execute the rule if the parameter was passed to our endpoint. Here is the actual implementation.

Simple yet powerful. The generic parameter is constrained to IPatchState<T>, which allows us to access IsBound from our request. In addition to having access to WhenBound, we still have access to all the capabilities of FluentValidation. The existing capabilities of FluentValidation allow us to write complex rules when necessary that go beyond just validating the state of the request, but also the state of our system.

Conclusion

Combining all three posts, we can create a simple yet productive approach to handling PATCH requests in our ASP.NET WebAPI applications. We also get a consistent approach that doesn’t add bloat to our controllers. Finally, we get to use our validation framework of choice. We hope you enjoyed this series, and if you have any questions, please feel free to leave a comment.

Update

As reader Brett M pointed out, we forgot to include the AbstractPatchStateRequest model in our series. So here it is. Sorry about that oversight, and thank you for reading.

publicabstractclassAbstractPatchStateRequest<TRequest,TModel>:IPatchState<TRequest,TModel>,IPatchState<TRequest>,IPatchStatewhereTRequest:class,IPatchState<TRequest,TModel>,new(){protectedreadonlyIList<string>boundProperties=newList<string>();privateBindingFlagsbindingFlags=BindingFlags.IgnoreCase|BindingFlags.Instance|BindingFlags.Public;protectedreadonlyIDictionary<string,Action<TModel>>patchStateMapping=newDictionary<string,Action<TModel>>(StringComparer.InvariantCultureIgnoreCase);publicvoidAddBoundProperty(stringpropertyName){if(!boundProperties.Contains(propertyName,StringComparer.InvariantCultureIgnoreCase)){boundProperties.Add(propertyName);}}publicTRequestAddPatchStateMapping<TProperty,TModelProperty>(Expression<Func<TRequest,TProperty>>propertyExpression,Expression<Func<TModel,TModelProperty>>modelMapping){varpropertyName=GetPropertyName(propertyExpression);varinstanceProperty=GetType().GetProperty(propertyName,bindingFlags);Action<TModel>mappingAction=(model)=>{BuildActionFromExpression(modelMapping)(model,(TModelProperty)instanceProperty.GetValue(this,null));};AddPatchStateMapping(propertyExpression,mappingAction);returnthisasTRequest;}publicTRequestAddPatchStateMapping<TProperty>(Expression<Func<TRequest,TProperty>>propertyExpression,Action<TModel>propertyToModelMapping=null){varpropertyName=GetPropertyName(propertyExpression);if(propertyToModelMapping==null){propertyToModelMapping=(model)=>{varmodelProperty=model.GetType().GetProperty(propertyName,bindingFlags);varinstanceProperty=GetType().GetProperty(propertyName,bindingFlags);if(modelProperty!=null&&instanceProperty!=null){modelProperty.SetValue(model,instanceProperty.GetValue(this,null),null);}};}if(patchStateMapping.ContainsKey(propertyName)){patchStateMapping[propertyName]=propertyToModelMapping;}else{patchStateMapping.Add(propertyName,propertyToModelMapping);}returnthisasTRequest;}privateAction<TObject,TProperty>BuildActionFromExpression<TObject,TProperty>(Expression<Func<TObject,TProperty>>accessor){if(accessor==null)thrownewArgumentNullException(nameof(accessor));varmemberExpression=accessor.BodyasMemberExpression;varmemberInfo=memberExpression?.Member;if(!(memberInfoisPropertyInfo)&&!(memberInfoisFieldInfo))thrownewInvalidOperationException("Member is not a property or field");varvalueParameter=Expression.Parameter(typeof(TProperty),"val");varassignmentExpression=Expression.Assign(memberExpression,valueParameter);varlambdaExpression=Expression.Lambda<Action<TObject,TProperty>>(assignmentExpression,accessor.Parameters[0],valueParameter);returnlambdaExpression.Compile();}privatestringGetPropertyName<TProperty>(Expression<Func<TRequest,TProperty>>propertyExpression){varpropertyBody=propertyExpression.BodyasMemberExpression;if(propertyBody==null){thrownewInvalidCastException($"Cannot get property name from {nameof(propertyExpression)}.");}else{varfullPropertyName=propertyBody.ToString();returnfullPropertyName.Substring(fullPropertyName.IndexOf('.')+1);}}publicboolIsBound<TProperty>(Expression<Func<TRequest,TProperty>>propertyExpression){varpropertyName=GetPropertyName(propertyExpression);returnboundProperties.Contains(propertyName,StringComparer.InvariantCultureIgnoreCase);}publicvoidPatch(TModelmodel){foreach(varkvpinpatchStateMapping){if(boundProperties.Contains(kvp.Key,StringComparer.InvariantCultureIgnoreCase)){kvp.Value(model);}}}}

Update 2016-10-11

To finish out the available code to make everything work, here are the patch-related interfaces: