Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using optional query parameters in F# Web Api project

I was converting a C# webapi project to F# using the F# ASP.NET templates. Everything is working great except optional query parameters. I keep getting this error

{
    "message": "The request is invalid.",
    "messageDetail": "The parameters dictionary contains an invalid entry for parameter 'start' for method 'System.Threading.Tasks.Task`1[System.Net.Http.HttpResponseMessage] GetVendorFiles(Int32, System.Nullable`1[System.DateTime])' in 'Thor.WebApi.VendorFilesController'. The dictionary contains a value of type 'System.Reflection.Missing', but the parameter requires a value of type 'System.Nullable`1[System.DateTime]'."
}

F# function signature:

[<HttpGet; Route("")>]
member x.GetVendorFiles( [<Optional; DefaultParameterValue(100)>] count, [<Optional; DefaultParameterValue(null)>] start : Nullable<DateTime> ) =

C# function signature:

[HttpGet]
[Route("")]
public async Task<HttpResponseMessage> GetVendorFiles(int count = 100,DateTime? start = null)

Does anyone know of any workarounds?

Updated: I figured out the cause of this issue. ASP.NET extracts default values for controller actions using ParameterInfo. Apparently the F# compiler doesn't compile default values the same way as C# does (even with the DefaultParameterValueAttribute)

What's the best way or working around this? Would it be some filter that I need to inject or implement my own ParameterBinding?

like image 644
Pavel Avatar asked Jan 26 '15 22:01

Pavel


People also ask

Can query parameters be optional?

As query parameters are not a fixed part of a path, they can be optional and can have default values.

What is difference between @RequestParam and @QueryParam?

@QueryParam is a JAX-RS framework annotation and @RequestParam is from Spring. QueryParam is from another framework and you are mentioning Spring. @Flao wrote that @RequestParam is from Spring and that should be used in Spring MVC.

How do I pass a query param on a map?

Create new map and put there parameters you need: Map<String, String> params = new HashMap<>(); params. put("param1", "value1"); params. put("param2", "value2");


3 Answers

You can work around this with a custom ActionFilterAttribute implementation. The following code supports the use of DefaultParameterValue values when an action argument is missing and when an action argument has a value of type System.Reflection.Missing.

The code is also in the Gist ActionFilter for ASP.NET Web API DefaultParameterValue support in F#.

namespace System.Web.Http

open System.Reflection
open System.Web.Http.Filters
open System.Web.Http.Controllers
open System.Runtime.InteropServices

/// Responsible for populating missing action arguments from DefaultParameterValueAttribute values.
/// Created to handle this issue https://github.com/aspnet/Mvc/issues/1923
/// Note: This is for later version of System.Web.Http but could be back-ported.
type DefaultParameterValueFixupFilter() =
  inherit ActionFilterAttribute()
  /// Get list of (paramInfo, defValue) tuples for params where DefaultParameterValueAttribute is present.
  let getDefParamVals (parameters:ParameterInfo array) =
    [ for param in parameters do
        let defParamValAttrs = param.GetCustomAttributes<DefaultParameterValueAttribute>() |> List.ofSeq 
        match defParamValAttrs with
        // Review: we are ignoring null defaults.  Is this correct?
        | [x] -> if x.Value = null then () else yield param, x.Value
        | [] -> () 
        | _ -> failwith "Multiple DefaultParameterValueAttribute on param '%s'!" param.Name
    ]
  /// Add action arg default values where specified in DefaultParameterValueAttribute attrs.
  let addActionArgDefsFromDefParamValAttrs (context:HttpActionContext) =  
    match context.ActionDescriptor with
    | :? ReflectedHttpActionDescriptor as ad ->
      let defParamVals = getDefParamVals (ad.MethodInfo.GetParameters())
      for (param, value) in defParamVals do
        match context.ActionArguments.TryGetValue(param.Name) with
        | true, :? System.Reflection.Missing
        | false, _ ->
          // Remove is null-op if key not found, so we handle both match cases OK.
          let _ = context.ActionArguments.Remove(param.Name)
          context.ActionArguments.Add(param.Name, value)
        | _, _ -> ()
    | _ -> ()
  /// Override adding suport for DefaultParameterValueAttribute values.
  override x.OnActionExecuting(context) =
    addActionArgDefsFromDefParamValAttrs context
    base.OnActionExecuting(context)
  /// Override adding suport for DefaultParameterValueAttribute values.
  override x.OnActionExecutingAsync(context, cancellationToken) =
    addActionArgDefsFromDefParamValAttrs context
    base.OnActionExecutingAsync(context, cancellationToken)
like image 142
bentayloruk Avatar answered Oct 27 '22 01:10

bentayloruk


You could use HttpRequestMessage and parse the query string dictionary.

EDITED...

To use HttpRequestMessage, let's say you have a query string page:

[<Route("api/Applications")>]
[<HttpGet()>]
member x.Get (message:HttpRequestMessage) = 
    let page = GetQueryStringParameter(message, "page") 

I would probably use a helper method to parse the query string out of the HttpRequestMessage, something like this:

let GetQueryStringParameter (message:HttpRequestMessage, value) = 
    let value = List.ofSeq ( message.GetQueryNameValuePairs() )
                |> List.tryFind (fun item -> item.Key = value)
    match value with
    | Some x -> x.Value
    | None   -> ""
like image 3
supermitsuba Avatar answered Oct 27 '22 02:10

supermitsuba


You're pretty much limited to workarounds, it seems.

What about using:

[<HttpGet; Route("")>]
member x.GetVendorFiles(count: Nullable<int>, start : Nullable<DateTime>) =
    let count = count.GetValueOrDefault(100)
    // or: let count = if count.HasValue then count.Value else 100
like image 3
Christopher Stevenson Avatar answered Oct 27 '22 01:10

Christopher Stevenson