So, hopefully this self-answer isn't poor form on S.O., but with a little prodding from Cj S.'s answer, I looked more into the Web API message lifecycle, and ended up creating an Action Filter:
public class QueryParamMatchingActionFilter : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext filterContext)
{
List<string> queryParamNames = filterContext.Request.GetQueryNameValuePairs().Select(q => (string)q.Key.ToLowerInvariant()).ToList();
List<string> methodParams = filterContext.ActionArguments.Select(q => (string)q.Key.ToLowerInvariant()).ToList();
List<string> unrecognized = queryParamNames.Where(qp => !methodParams.Any(mp => mp == qp)).ToList();
if (unrecognized.Count > 0)
{
List<string> errors;
if (filterContext.Request.Properties.ContainsKey("MY_ERRORS"))
errors = (List<string>)filterContext.Request.Properties["MY_ERRORS"];
else
errors = new List<string>();
foreach (string badParam in unrecognized)
{
errors.Add(String.Format("UNRECOGNIZED PARAMETER IGNORED: {0}", badParam));
}
filterContext.Request.Properties["MY_ERRORS"] = errors;
}
}
}
So now I can just decorate my controllers with "[QueryParamMatchingActionFilter]". The contents of MY_ERRORS get put into the response by a DelegatingHandler I already had setup, which wraps responses with some useful metadata. But this code should be easy enough to repurpose toward doing other things with extraneous params. The ability to use the ActionArguments property of filterContext means we get to skip using reflection, but I still wouldn't be surprised if someone else knew of a more efficient way to do this!