{"id":20168208,"url":"https://github.com/russbaz/webframe","last_synced_at":"2025-07-06T15:03:25.679Z","repository":{"id":56810879,"uuid":"418775296","full_name":"RussBaz/WebFrame","owner":"RussBaz","description":"F# framework for rapid prototyping with ASP.NET Core.","archived":false,"fork":false,"pushed_at":"2022-03-20T12:48:42.000Z","size":484,"stargazers_count":104,"open_issues_count":0,"forks_count":4,"subscribers_count":5,"default_branch":"master","last_synced_at":"2025-07-06T15:03:22.782Z","etag":null,"topics":["api","asp-net-core","backend","fsharp","prototyping","web","web-framework"],"latest_commit_sha":null,"homepage":"","language":"F#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/RussBaz.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2021-10-19T04:56:19.000Z","updated_at":"2024-08-10T11:52:32.000Z","dependencies_parsed_at":"2022-09-11T02:30:57.057Z","dependency_job_id":null,"html_url":"https://github.com/RussBaz/WebFrame","commit_stats":null,"previous_names":[],"tags_count":19,"template":false,"template_full_name":null,"purl":"pkg:github/RussBaz/WebFrame","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RussBaz%2FWebFrame","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RussBaz%2FWebFrame/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RussBaz%2FWebFrame/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RussBaz%2FWebFrame/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/RussBaz","download_url":"https://codeload.github.com/RussBaz/WebFrame/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/RussBaz%2FWebFrame/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":263922473,"owners_count":23530334,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["api","asp-net-core","backend","fsharp","prototyping","web","web-framework"],"created_at":"2024-11-14T01:06:44.801Z","updated_at":"2025-07-06T15:03:25.624Z","avatar_url":"https://github.com/RussBaz.png","language":"F#","readme":"# WebFrame\n[![Build Status](https://img.shields.io/github/workflow/status/RussBaz/WebFrame/.NET%20Core)](https://github.com/russbaz/webframe/actions/workflows/github-actions.yml)\n[![Latest Published Nuget Version](https://img.shields.io/nuget/v/RussBaz.WebFrame)](https://www.nuget.org/packages/RussBaz.WebFrame/)\n[![Latest Published Templates Version](https://img.shields.io/nuget/v/RussBaz.WebFrame.Templates?label=templates)](https://www.nuget.org/packages/RussBaz.WebFrame.Templates/)\n\nF# framework for rapid prototyping with ASP.NET Core.\n\n### Fast Travel\n* [Introduction - A Story Time](#introduction---a-story-time)\n* [Guiding Principles](#guiding-principles)\n* [Setup](#setup)\n  * [For the first timers](#for-the-first-timers)\n  * [For an advanced audience](#for-an-advanced-audience)\n* [Examples](#examples)\n  * [Sample Code](#sample-code)\n* [Documentation](#documentation)\n  * [Main App](#main-app)\n  * [Request Handling](#request-handling)\n  * [Request Services](#request-services)\n    * [Path Parts](#path-parts)\n    * [Route Parts](#route-parts)\n    * [Query Parts](#query-parts)\n    * [Header Parts](#header-parts)\n    * [Cookie Parts](#cookie-parts)\n    * [Config Parts](#config-parts)\n    * [Body Parts](#body-parts)\n      * [Form](#form)\n      * [Json](#json)\n    * [Globalization](#globalization)\n    * [Request Logging](#request-logging)\n  * [DI Outside of Request](#di-outside-of-request)\n  * [Host Logging](#host-logging)\n  * [System Configuration](#system-configuration)\n  * [Request Helpers](#request-helpers)\n  * [Modules](#modules)\n  * [Testing](#testing)\n  * [Exceptions](#exceptions)\n* [Changelog](#changelog)\n\n## Introduction - A Story Time\nSome long time ago I used to write web stuff using Python frameworks such as Django. More recently I got deeply into the F#. It satisfies a lot of my requirements. However, I was not satisfied with the current state of the F# web development. Every time I tried to write something quickly, I often had to choose between a heavily functional programming oriented frameworks or extremely tedious ASP.NET Core.\n\nI wanted something quick. Why couldn't I just do the following?\n\n```F#\nopen WebFrame\n\nlet app = App ()\n\napp.Get \"/\" \u003c- fun serv -\u003e serv.EndResponse \"Hello World!\"\napp.Run ()\n```\n\nSo I did write it myself!\n\nYes, you can just write it and experience the full ASP.NET Core server!\n\nThere are a lot of helper methods available and mostly all of them are attached to the only (RequestServices) parameter that is passed to the handler on each request. This setup uses the endpoints api and all the routes can be inspected at any time.\n\nThis project is still a work in progress and it is far from being a final product. Therefore - all contributions are absolutely welcome.\n\n## Guiding Principles\nHere are the guiding principals for the development and vision of the project:\n* The common scenario should require the least amount of code\n* The code should be obvious, self descriptive and visible\n* Prefer existing F# syntax and avoid custom operators. Overloading is OK when it helps with previous points.\n* Be recognisable to non-FP developers and web developers from other languages\n* Make it easy to drop down to raw ASP.NET Core when the case requires it\n\nTherefore, explicit is better than implicit but the speed of prototyping must always be considered. It is a number one priority.\n\n* And finally, a beautiful code (in my eyes) is a better code.\n\n## Setup\nBefore you begin, make sure that [.NET](https://dotnet.microsoft.com/download) 6.0+ is installed and reachable from your terminal (i.e. it is present in the Path environment variable)\n### For the first timers\nFor those who just start with F#, I recommend starting with the following website ['F# for Fun and Profit: F# syntax in 60 seconds'](https://fsharpforfunandprofit.com/posts/fsharp-in-60-seconds/).\n\nOnce you familiarise yourself with the syntax and deal with the .Net runtime, you should check the `Samples` folder.\n\nInstall the minimal template:\n\n```\ndotnet new -i \"RussBaz.WebFrame.Templates::*\"\n```\n\nCreate a new project just like you would normally do in a new directory of your choice:\n\n```\ndotnet new webframe\n```\n\nOnce this is done, run the following command (in the same folder where your .fsproj file is) to start the server:\n\n`dotnet run`\n\nNote: you may need to restore the project before your IDE can correctly work with the project: `dotnet restore` and `dotnet build`\n\nRecommended editors by personal preferences for F#:\n* VS Code with Ionide-fsharp extension\n* JetBrains Rider\n* Visual Studio\n\n### For an advanced audience\nCreate a new console or an empty asp core project with F#.\n\nIf it is a console project, add Web framework reference.\n\nIf it is a Web project, delete the Setup file and clean up the Program file.\n\nAdd WebFrame package reference and open it in the main file. It will immediately import all the required stuff for the minimal setup.\n\nPlease consider using [Paket](https://fsprojects.github.io/Paket/) if you do not mind (it can reference GitHub projects directly)\n\nUpdate the dependencies if required.\n\n## Examples\nPlease check the Samples folder for examples of most of available apis.\n* Minimal - most basic setup\n* Modules - shows how to modularise the app\n* LocalServer - shows how to work with the static files. It sets up a server that will share the specified folder (first command line argument) on the local network.\n* TestServer - shows how you can access a virtual test server that can be used for testing. You can also check out the WebFrame.Test folder for more details on how to use it.\n* StandardServer - shows common scenarios \n* AdvancedServer - a kitchen sink of most other available apis and helpers from the simplest to the most complicated\n\n### Sample Code\nThe following snippet shows some common scenarios.\n```F#\nopen WebFrame\nopen type WebFrame.Endpoints.Helpers\n\n//Sample exceptions\nexception TeapotException\n\ntype ItemAlreadyExistsException ( itemName: string ) =\n    inherit System.Exception $\"Item {itemName} already exists.\"\n\n[\u003cEntryPoint\u003e]\nlet main argv =\n    let items = [ \"todo1\"; \"todo2\"; \"todo3\" ]\n    \n    let api = AppModule \"/api\"\n    \n    // Whenever a TeapotException is thrown in this module\n    // It will be returning code 418\n    api.Errors \u003c- Error.codeFor\u003cTeapotException\u003e 418\n    \n    // And the same with \"ItemAlreadyExistsException\"\n    // You can even catch the automatically captured exceptions\n    api.Errors \u003c- Error.codeFor\u003cItemAlreadyExistsException\u003e 409\n    \n    // Returning items\n    api.Get \"/\" \u003c- fun serv -\u003e\n        serv.EndResponse items\n        \n    // Adding items\n    // By sending an item Name as a string field in a form\n    api.Post \"/\" \u003c- fun serv -\u003e\n        // If a required property in user input is not found,\n        // then 400 error is issued automatically\n        let itemName = serv.Body.Form.Required\u003cstring\u003e \"name\"\n        \n        // If you need to check the content type, you can try:\n        \n        // Is it a form? (bool property)\n        // serv.Body.Form.IsPresent\n        \n        // Is it a json conent type? (bool property)\n        // serv.Body.Json.IsJsonContentType\n        \n        // Is it a json (checks the content type)?\n        // If yes, try validating it. (bool task method)\n        // serv.Body.Json.IsPresent ()\n        \n        // In all other cases (string property):\n        // serv.ContentType\n        \n        // ContentType is an empty string if the header is missing\n        \n        // To set a response Content-Type manually and quickly,\n        // Just assign the required value to the same property\n        // Reading it will still return the content type of the request\n        serv.ContentType \u003c- \"text/plain\"\n        \n        // This exception was already registered in the module\n        // and it will be automatically handled\n        if items |\u003e List.contains itemName then raise ( ItemAlreadyExistsException itemName )\n        \n        if itemName = \"coffee\" then raise TeapotException\n        \n        serv.StatusCode \u003c- 201\n        printfn $\"Faking a successful addition of a new item {itemName}\"\n    \n        serv.EndResponse ()\n    \n    // Passing command arguments to the ASP.NET Core server\n    let app = App argv\n    \n    // Serving Static Files is disabled by default\n    app.Services.StaticFiles.Enabled \u003c- true\n    \n    // Optionally adding a prefix to all static files\n    app.Services.StaticFiles.Route \u003c- \"/static\"\n    \n    // Please check the LocalServer sample for more information on the static files\n    \n    // Overriding config\n    app.Config.[ \"Hello\" ] \u003c- \"World\"\n    // Overriding connecition string for \"MyConnection\"\n    app.Config.ConnectionStrings \"MyConnection\" \u003c- \"host=localhost;\"\n    \n    app.Get \"/\" \u003c- page \"Pages/Index.html\"\n    app.Get \"/About\" \u003c- page \"Pages/About.html\"\n    \n    app.Get \"/Hello\" \u003c- fun serv -\u003e\n        // Accessing configuration at runtime\n        // The field naming syntax is the same as in ASP.NET Core\n        let hello = serv.Config.Required\u003cstring\u003e \"hello\"\n        // Always present as well as some other web host properties\n        let isDevelopment = serv.Config.IsDevelopment\n        \n        serv.EndResponse $\"[ {hello} ] Dev mode: {isDevelopment}\"\n    \n    app.Module \"ToDoApi\" \u003c- api\n    \n    // Running on the default ports if not overriden in the settings\n    app.Run ()\n    \n    0 // exit code\n```\n## Documentation\n### Main App\nHow to create, build and run a WebFrame app\n```F#\nopen System\nopen WebFrame\n\nlet argv = Environment.GetCommandLineArgs ()\n\n// All it takes to create an app\nlet app = App ()\n\n// If you want to pass command line arguments\n// to the underlying ASP.NET Core server\nlet app = App argv\n\n// To run an app (blocking mode)\n// If the app is not yet built\n// it will run the build step automatically\n// However, it will not rebuild the app if it is already built\napp.Run ()\n\n// You can also specify directly the connection urls in this step\n// This will override all existing 'urls' configurations\n// Furthermore, it will force the app to be rebuilt even if it is already built\napp.Run [ \"http://localhost:5000\" ]\n\n// To Build an app manually before running it\n// One can run this optional command\n// Once the app is built, further changes to configs or endpoints\n// will not take place untill the app is rebuilt\napp.Build ()\n\n// If you need to adjust some default WebFrame constants\n// You can do the following\nopen WebFrame.Configuration\n\nlet defaults = { SystemDefaults.standard with SettingsPrefix = \"MyApp\" }\nlet app = App defaults\n\n// You can still pass the args\nlet defaults = { defaults with Args = args }\nlet app = App defaults\n\n// Lastly, if your app needs an access to dependency injection outside of request\n// for example for writing an extension,\n// you can request an IServiceProvider from the app directly\n// The app has to be built when the method is requested\n// If the app is rebuilt after requesting the provider,\n// just call the method again to receive a fresh copy\nlet serviceProvider = app.GetServiceProvider ()\n\n// To access the inbuilt logger before the host is built\n// For more details check the host logging section\napp.Log\n\n// If you need to execute something before and after the server runs\n// You can add on start and on stop hooks\n// The hook is a function that takes an instance of App and returns an unit\napp.Hooks.AddOnStartHook ( fun a -\u003e () )\napp.Hooks.AddOnStopHook ( fun a -\u003e () )\n// and you can clear them all with the following methods\napp.Hooks.ClearOnStartHooks ()\napp.Hooks.ClearOnStopHooks ()\n\n// For additional options for adjusting the defaults,\n// please check the configuration section of the docs\n```\n### Request Handling\nHow to process incoming requests\n```F#\n// There are two types of request handlers\n// Each returning the HttpWorkload in some form\n// Internally, all the handlers are converted into TaskHttpHandler\ntype HttpWorkload =\n    | EndResponse            // ends the response processing\n    | TextResponse of string // the resonse body as string, default content-type: text/plain\n    | HtmlResponse of string // the resonse body as string, default content-type: text/html\n    | FileResponse of string // filename of the file to be returned\n    | JsonResponse of obj    // an obj to be serialised and returned as json\n\n// Internal handlers\ntype HttpHandler = HttpContext -\u003e HttpWorkload\ntype TaskHttpHandler = HttpContext -\u003e Task\u003cHttpWorkload\u003e\n\n// User provided handlers, that are converted to the internal representation\ntype ServicedHandler = RequestServices -\u003e HttpWorkload\ntype TaskServicedHandler = RequestServices -\u003e Task\u003cHttpWorkload\u003e\n\n// Use EndResponse when you plan to construct the response manually\n// And do not want any further processing to be applied after that\n// If no response is provided at all, a default empty bodied 200 response is returned\n// If you return a workload that contradicts your manual changes to the response object\n// Then normally a ServerException would be thrown\n\n// In order to define a request handler,\n// Pass a function to an indexed property named after the expected Http Method\n// The provided indexer is a string that will be converted into ASP.Net Core RoutePattern\n// MS Docs Reference on Route Templating Syntax\n// https://docs.microsoft.com/en-us/aspnet/core/fundamentals/routing?view=aspnetcore-5.0#route-template-reference \napp.Get \"/\" \u003c- fun serv -\u003e serv.EndResponse ()\napp.Post \"/\" \u003c- fun serv -\u003e serv.EndResponse ()\napp.Put \"/\" \u003c- fun serv -\u003e serv.EndResponse ()\napp.Patch \"/\" \u003c- fun serv -\u003e serv.EndResponse ()\napp.Delete \"/\" \u003c- fun serv -\u003e serv.EndResponse ()\napp.Head \"/\" \u003c- fun serv -\u003e serv.EndResponse ()\napp.Options \"/\" \u003c- fun serv -\u003e serv.EndResponse ()\napp.Connect \"/\" \u003c- fun serv -\u003e serv.EndResponse ()\napp.Trace \"/\" \u003c- fun serv -\u003e serv.EndResponse ()\n\n// If you need to perform an ayncronous computation\napp.GetTask \"/\" \u003c- fun serv -\u003e task { return serv.EndResponse () }\napp.PostTask \"/\" \u003c- fun serv -\u003e task { return serv.EndResponse () }\napp.PutTask \"/\" \u003c- fun serv -\u003e task { return serv.EndResponse () }\napp.PatchTask \"/\" \u003c- fun serv -\u003e task { return serv.EndResponse () }\napp.DeleteTask \"/\" \u003c- fun serv -\u003e task { return serv.EndResponse () }\napp.HeadTask \"/\" \u003c- fun serv -\u003e task { return serv.EndResponse () }\napp.OptionsTask \"/\" \u003c- fun serv -\u003e task { return serv.EndResponse () }\napp.ConnectTask \"/\" \u003c- fun serv -\u003e task { return serv.EndResponse () }\napp.TraceTask \"/\" \u003c- fun serv -\u003e task { return serv.EndResponse () }\n\n// You do not need to use EndResponse convinience method of the RequestService parameter\n// Your handler can return an HttpWorkload directly\nopen WebFrame.Http\n\napp.Get \"/home\" \u003c- fun _ -\u003e TextResponse \"Hello World\"\napp.PostTask \"/home\" \u003c- fun _ -\u003e task { return TextResponse \"Hello World\" }\n```\nIf an `InputException` is raised during the request handling then a `400` response with the exception message would returned. In case of `ServerException`, a `500` response is issued instead.\n### Request Services\n`RequestServices` object is passed to all user defined `ServicedHandler` and it is the secret sauce of this framework. It contains intuitively named properties to access different parts of the request and the response. Furthermore, it encapsulates configuration, routes and services (DI). \n\nIt also provides different helpers for many common scenarios. You can even access the raw `HttpRequest` and its properties through it if necessary.\n\nThe main rule of thumb that Request Services are trying to follow is:\n* When you read a property, you are reading from a request object\n* When you write to a property, then you are writing to a response object\n* The same applies to the methods (where appropriate)\n  * Different `Get`s would normally extract a value from the request\n  * Different `Set`s would normally set a value in the response\n  * Etc.\n\nList of available 'services':\n```F#\n// serv: RequestServices\n// Defined in: Services\n\n// Raw ASP.NET Core HttpContext\nserv.Context\n\n// Request path properties\n// Check path Parts for details\nserv.Path\n\n// Request route parameters services\n// Check Route Parts for details\nserv.Route\n\n// Request query parameters services\n// Check Query Parts for details\nserv.Query\n\n// Request and response header services\n// Check Header Parts for details\nserv.Headers\n\n// Request and response cookie services\n// Check Cookies Parts for details\nserv.Cookies\n\n// Server configuration services\n// Check Config Parts for details\nserv.Config\n\n// Request body services\n// Check Body Parts for details\nserv.Body\n\n// Globalization services\n// Check the Globalization section for details\nserv.Globalization\n\n// Server route services\nserv.AppRoutes\n\n// Set response status code\nserv.StatusCode \u003c- 201\n\n// Get request Content-Type\nserv.ContentType\n// Set response Content-Type\n// Overrides all the existing content-type data on the response\nserv.ContentType \u003c- \"text/plain\"\n\n// Get ASP.NET Core service\n// Throws MissingRequiredDependencyException on failure\nserv.Services.Required\u003cIRandomService\u003e ()\n// Returns optional instead of raising exceptions\nserv.Services.Optional\u003cIRandomService\u003e ()\n// If it fails to find registered service, then\n// it returns the result of the default providing function\nserv.Services.Get\u003cIRandomService\u003e RandomService\n\n// Get the ASP.NET Core endpoint associated with this request\nserv.GetEndpoint\n\n// Get the description of the currently taken route\nserv.RouteDescription\n\n// Possible properties\nserv.RouteDescription.Name\nserv.RouteDescription.Pattern\nserv.RouteDescription.Description\n\n// Enable request buffering in ASP.NET Core\nserv.EnableBuffering ()\n\n// Get ILogger for a category called \"MyLogCategory\"\nserv.LoggerFor \"MyLogCategory\"\n\n// Get simple Logger for the current request\n// Check Request Logging for further details\nserv.Log\n\n// HttpWorkload helpers\n// Must be the final result returned by the handler\n\n// Redirect helper\n// 302 to '/'\nserv.Redirect \"/\"\n// 302 to '/'\nserv.Redirect ( \"/\", false )\n// 301 to '/'\nserv.Redirect ( \"/\", true )\n\n// FileResponse\n// Return a file named \"myfile.txt\"\nserv.File \"myfile.txt\"\n// Return a file \"image.png\" and set the content-type manually to \"image/png\"\nserv.File ( \"image.png\", \"image/png\" )\n\n// Template rendering response\n// Uses DotLiquid templating by default but can be replaced\n// The default template root is set to content root\n// For more information on DotLiquid, visit their website at:\n// dotliquidmarkup.org\n// https://github.com/dotliquid/dotliquid\n\n// To render a template and insert data into it,\n// use the following method:\n// It returns a HtmlWorkload with the template rendered as a string\nserv.Page \"/path/from/template/root/to/template\" {| Name = \"John\" |}\n\n// Return a text reponse\nserv.EndResponse \"hello world\"\n\n// Return Json response\n// Pass any non-string value and it will try send it as json\n// If the object cannot be parsed as a valid json it will attempt serialising it as string instead\nserv.EndResponse {| Value = \"hello world\" |}\n\n// End a response\n// Used to send an empty bodied response\n// or when manually constructing the response\nserv.EndResponse ()\n```\n#### Path Parts\nPath parts are read only properties describing the path of currently processed web request.\n```F#\nserv.Path.Method // string property\nserv.Path.Protocol // string property\nserv.Path.Scheme // string property\nserv.Path.Host // string property\nserv.Path.Port // int option property\n// Path Base is a part of ASP.NET Core\n// Most of the time it is an empty string\n// It can be useful in some hosting scenarios\n// but requires an additional middleware to be activated\n// Please refer to this stackoverflow question:\n// https://stackoverflow.com/questions/58614864/whats-the-difference-between-httprequest-path-and-httprequest-pathbase-in-asp-n\nserv.Path.PathBase // string property\nserv.Path.Path // string property\nserv.Path.QueryString // string property\nserv.Path.IsHttps // bool property\n```\n#### Route Parts\nRoute parts services help with reading, parsing and validating the route template properties.\n```F#\n// The route template syntax is the ASP.NET Core default syntax\n// Here is the link to the docs:\n// https://docs.microsoft.com/en-us/aspnet/core/fundamentals/routing?view=aspnetcore-6.0#route-template-reference\napp.Get \"/resource/{id:guid}/{property?}\" \u003c- fun serv -\u003e\n    // The following line reads the \"id\" route property and tries to parse it into Guid\n    // if it is absent or cannot be parsed,\n    // then MissingRequiredRouteParameterException is raised\n    let resId = serv.Route.Required\u003cGuid\u003e \"id\"\n    // Will return a string optional with the value of \"property\" route segment\n    let property = serv.Route.Optional\u003cstring\u003e \"property\"\n    // Alternatively, you can specify the default value for the optional segment inline\n    let property = serv.Route.Get \"property\" \"none\"\n    // If you just want a string option without any parsing, then use:\n    let property = serv.Route.String \"property\"\n    // NOTE, this does not override the default ASP.NET Core template matching and type checking\n    \n    // Accessing raw RouteValueDictionary\n    let raw = serv.Route.Raw\n    serv.EndResponse ()\n```\n#### Query Parts\nQuery parts services help with reading, parsing and validating the request query parameters.\n```F#\napp.Get \"/resource/\" \u003c- fun serv -\u003e\n    // Trying to read query parameter \"q\" as a string\n    // Can only be None if it is missing as there is no parsing involved\n    let res = serv.Query.String \"q\"\n    // This will try getting \"q\" query parameter and parse it into int\n    // It will throw MissingRequiredQueryParameterException if it fails to read or parse\n    let res = serv.Query.Required\u003cint\u003e \"q\"\n    // If reading or parsing fails, then it will return None\n    let res = serv.Query.Optional\u003cint\u003e \"q\"\n    // Trying to read and parse the \"q\" query parameter with a default value\n    let res = serv.Query.Get \"q\" 10\n    \n    // If you were expecting multiple query parameters with the same name\n    // Use commands prefixed with All\n    \n    // Never fails. Returns a list of \"q\" parameters that managed to parse into the specified type\n    let res = serv.Query.All\u003cint\u003e \"q\"\n    // Returns all \"q\" parameters as strings\n    // Returns None if none is found\n    let res = serv.Query.AllString \"q\"\n    // It will now attempt to parse the string values into the specified type\n    // If any fails - it will throw MissingRequiredQueryParameterException\n    let res = serv.Query.AllRequired\u003cint\u003e \"q\"\n    // It will now attempt parsing the string values into the specified type\n    // Returns None if any conversion fails\n    let res = serv.Query.AllOptional\u003cint\u003e \"q\"\n    \n    // This will return the number of times \"q\" parameter was specified in the request\n    // It is 0 if the query parameter is missing\n    let count = serv.Query.Count \"q\"\n    \n    // Accessing the raw IQueryCollection\n    let raw = serv.Query.Raw\n    \n    serv.EndResponse ()\n```\n#### Header Parts\nHeader parts services help with reading, parsing and setting request and response headers.\n```F#\napp.Get \"/\" \u003c- fun serv -\u003e\n    // Next methods only return the first found value\n    // Returns the request header named \"custom\"\n    // Will throw MissingRequiredHeaderException if not found\n    let h = serv.Headers.Required \"custom\"\n    // Returns the request header named \"custom\"\n    // None if not found\n    let h = serv.Headers.Optional \"custom\"\n    // Tries reading the request header named \"custom\"\n    // If it is not found, then a default value is returned\n    let h = serv.Headers.Get \"custom\" \"myvalue\"\n    \n    // Returns a list of comma separated values from the request header named \"custom\"\n    // Returns an empty list if not found\n    let h = serv.Headers.All \"custom\"\n    \n    // Replaces response header named \"custom\" with a comma separated list of values\n    serv.Headers.Set \"custom\" [ \"myvalue\" ]\n    // Appends an additional value to the response header named \"custom\"\n    serv.Headers.Append \"custom\" \"myvalue\"\n    // Removes the response header named \"custom\"\n    serv.Headers.Delete \"custom\"\n    \n    // Counting how many comma seaparated segments request header \"custom\" has\n    // If it is 0 if the header is missing\n    let count = serv.Headers.Count \"count\"\n    \n    // Getting raw request IHeaderCollection\n    let inHeaders = serv.Headers.Raw.In\n    // Getting raw response IHeaderCollection\n    let outHeaders = serv.Headers.Raw.Out\n    \n    serv.EndResponse ()\n```\n#### Cookie Parts\nCookie parts services help with reading, parsing and setting request and response cookies.\n```F#\napp.Get \"/\" \u003c- fun serv -\u003e\n    // Trying to read \"mycookie\" cookie from the request\n    // If not found, then MissingRequiredCookieException is thrown \n    let c = serv.Cookies.Required \"mycookie\"\n    // Trying to read the request cookie\n    // Returns None if not found\n    let c = serv.Cookies.Optional \"mycookie\"\n    // Trying to read the request cookie\n    // And if not found, then the default value is returned\n    let c = serv.Cookies.Get \"mycookies\" \"myvalue\"\n    \n    // Asking to set a cookie to the specified value\n    serv.Cookies.Set \"mycookie\" \"myvalue\"\n    \n    // Cookie options is an ASP class\n    // open Microsoft.AspNetCore.Http\n    let emptyOpts = CookieOptions ()\n    \n    // Requesting a new cookie with additional options\n    serv.Cookies.SetWithOptions \"mycookies\" \"myvalue\" emptyOpts\n    \n    // Requesting a removal of the cookie\n    serv.Cookies.Delete \"mycookies\"\n    // Requesting a cookie removal with additional options\n    serv.Cookies.DeleteWithOptions \"mycookie\" emptyOpts\n    \n    // Getting a raw IRequestCookieCollection\n    let rawIn = Serv.Cookies.Raw.In\n    // Getting a raw IResponseCookies\n    let rawOut = Serv.Cookies.Raw.Out\n    \n    serv.EndResponse ()\n```\n#### Config Parts\nConfig parts services help with reading, parsing and validating server settings.\n\n`WebFrame` uses default ASP.NET Core configuration options.\n\nIn addition, it provides ways to quickly override the configs with inmemory values.\n```F#\nlet app = App argv\n\n// Overriding the config with custom values\napp.Config.[ \"Hello:Wolrd\" ] \u003c- \"Value\"\n// Overriding connecition string for \"MyConnection\"\napp.Config.ConnectionStrings \"MyConnection\" \u003c- \"host=localhost;\"\n\napp.Get \"/\" \u003c- fun serv -\u003e\n    // NOTE: Missing configs are Server Exceptions\n    // and therefore will result 5xx errors\n\n    // Trying to read config with given keys\n    // Returns None if not found\n    let c = serv.Config.String \"Hello:World\"\n    // It will throw MissingRequiredConfigException if the key is not found\n    // or cannot be parsed\n    let c = serv.Config.Required\u003cstring\u003e \"Hello:World\"\n    // Returns None if not found or cannot be parsed\n    let c = serv.Config.Optional\u003cstring\u003e \"Hello:World\"\n    // Returns a default value if not found or cannot be parsed\n    let c = serv.Config.Get \"Hello:World\" \"N/A\"\n    \n    // Helpers for dealing with environments\n    \n    // String properties (fixed for every request)\n    let a = serv.Config.ApplicationName\n    let e = serv.Config.EnvironmentName\n    \n    // Bool properties (fixed for every request)\n    let r = serv.Config.IsDevelopment\n    let r = serv.Config.IsStaging\n    let r = serv.Config.IsProduction\n    \n    // Returns true if teh environment name matches the provided one\n    let r = serv.Config.IsEnvironment \"EnvironmentName\"\n    \n    serv.EndResponse ()\n```\n#### Body Parts\nThe body parts services allow quick parsing and validation of different types of request bodies during the request handling.\n```F#\n// If you expect a form encoded body, use the following\nserv.Body.Form\n\n// If you expect a json encoded body, use this\nserv.Body.Json\n\n// In case you need to read a raw body as a stream\nserv.Body.Raw\n\n// or if you want to get a body pipe reader\nserv.Body.RawPipe\n```\n##### Form\nServices for dealing with form encoded bodies.\n```F#\napp.Post \"/resource\" \u003c- fun serv -\u003e\n    // Checking if the Content Type header is set properly on the request\n    let isPresent = serv.Body.Form.IsPresent\n\n    // trying to read the \"MyField\" field from the request body\n    // returns a string optional\n    // only fails if the field cannot be found\n    // returns the first field matching the name if multiple are present\n    let f = serv.Body.Form.String \"MyField\"\n    \n    // It will take the first matching field and it will try to parse it as int\n    // It raises MissingRequiredFormException if the request does not have a valid form encoded body\n    // Or raises MissingRequiredJsonFieldException if the field cannot be found or parsed\n    let f = serv.Body.Form.Required\u003cint\u003e \"MyField\"\n    \n    // The same as above but does not raise any exceptions on failure\n    // If the form or field are missing, or if the field cannot be parsed,\n    // then None is returned\n    let f = serv.Body.Form.Optional\u003cint\u003e \"MyField\"\n    \n    // If the field cannot be read, then returns a default value\n    let f = serv.Body.Form.Get \"MyField\" 42\n    \n    // Returns all the found \"MyFIeld\" fields and parses them into int\n    // If any of them fails, then returns an empty list\n    // If also returns an empty list if the form itself is not found\n    let fields = serv.Body.Form.All\u003cint\u003e \"MyField\"\n    \n    // The same as above but leaves all the values as strings\n    // It cannot fail due to parsing errors\n    let fields = serv.Body.Form.AllString \"MyField\"\n    \n    // It will take all the matching fields and it will try to parse them as int\n    // It raises MissingRequiredFormException if the request does not have a valid form encoded body\n    // Or raises MissingRequiredJsonFieldException if the field cannot be found\n    // It also raises MissingRequiredJsonFieldException if any of found fields cannot be parsed\n    let fields = serv.Body.Form.AllRequired\u003cint\u003e \"MyField\"\n    \n    // The same as AllRequired but will return None on any failure\n    let fields = serv.Body.Form.AllOptional\u003cint\u003e \"MyField\"\n    \n    // Returns the number of times the field has appeared in the form\n    // Returns zero if the form is missing.\n    let count = serv.Body.Form.Count \"MyField\"\n    \n    // Getting the raw IFormCollection from the request\n    // If the form cannot be parsed, then returns None\n    let form = serv.Body.Form.Raw\n    \n    // If the form has files, they can be accessed here\n    // Returns None if the form is missing\n    // Always present otherwise even if no files are available\n    let files = serv.Body.Form.Files\n    \n    // TODO: make retuned files unwrapped by the next release\n    // TODO: it should only fail when the Required method is called\n    \n    // Unwrapping option for simplicity\n    // Will throw Null Exceptions if None!\n    let files = files.Value\n    \n    // Getting the list of all provided files\n    // returns IFormFile list\n    let f = files.All ()\n    \n    // Getting the file descriptor from the form\n    // Raises MissingRequiredFormFileException if the expected filename is not found\n    let f = files.Required \"MyFileName\"\n    \n    // The same as above but returns None on failure\n    let f = files.Optional \"MyFileName\"\n    \n    // Returns the number of files received in the form\n    let fileCount = files.Count\n    \n    serv.EndResponse ()\n```\n##### Json\nServices for dealing with JSON encoded bodies.\n```F#\napp.PostTask \"/resource\" \u003c- fun serv -\u003e task {\n    // Reading a body and parsing as a specified type, even an anonymous one\n    // If the body cannot does not match they type, ewhether missing or additional properties are found,\n    // MissingRequiredJsonException is raised\n    let! data = serv.Body.Json.Exact\u003c{| Name: string; Age: int |}\u003e ()\n    \n    // Alternatively, you can pass either a json path string\n    // or a simplified Newtonsoft.Json query syntax\n    // to query the body field by field\n    \n    // Here we are getting a string option\n    // If the Json body is missing or the field is missing\n    // it will return None\n    \n    let! data = serv.Body.Json.String \"$.Name\"\n    let! data = serv.Body.Json.String \"Name\"\n    \n    // You can provide a default value for the field\n    let! data = serv.Body.Json.Get \"Name\" \"Default Value\"\n    \n    // If the query cannot return the field\n    // Then the MissingRequiredJsonFieldException is raised\n    // If the Json body itself cannot be parsed,\n    // then MissingRequiredJsonException is raised\n    let! data = serv.Body.Json.Required\u003cint\u003e \"Age\"\n    \n    // The same as above but errors return None instead\n    let! data = serv.Body.Json.Optional\u003cint\u003e \"Age\"\n    \n    // If you want to return multiple results qwith a single query\n    // If any found field fails to parae,\n    // it returns an empty list\n    let! data = serv.Body.Json.All\u003cint\u003e \"$.Points[:].X\"\n    // This was a finctional query to return property X from every object in the array Points as a list\n    \n    // The same as above but it cannot fail due to parsing errors\n    // as it returns a list of strings\n    // it returns an empty list in case of errors\n    let! data = serv.Body.Json.AllString \"$.Points[:].X\"\n    \n    // If any field fails to parse, then\n    // MissingRequiredJsonFieldException is raised\n    // if the json body cannot be parsed, then\n    // MissingRequiredJsonException is raised\n    let! data = serv.Body.Json.AllRequired\u003cint\u003e \"$.Points[:].X\"\n    \n    // The same as above but it returns None if any error is encountered\n    let! data = serv.Body.Json.AllOptional\u003cint\u003e \"$.Points[:].X\"\n    \n    // Returns the number of fields found to satisfy the query\n    // It does not take any type parsing into an account\n    // Also, if the json encoded body is missing,\n    // then it returns 0\n    let! count = serv.Body.Json.Count \"$.Points[:].X\"\n    \n    // If you need a raw JObject afor manual processing\n    // then you should use this method\n    let! j = serv.Body.Json.Raw \"Points\"\n    \n    // Confirms if the correct Content Type is received\n    let isJson = serv.Body.Json.IsJsonContentType\n    \n    // Confirms if the Json body is a valid json by parsing it into the memory\n    let! isJson = serv.Body.Json.IsPresent ()\n    \n    return serv.EndResponse ()\n}\n```\n#### Globalization\nServices for dealing with globalisation of your app.\n```F#\napp.Get \"/culture\" \u003c- fun serv -\u003e\n    // If the request has a valid Accept-Language header\n    // then it tries to convert it into an associated CultureInfo\n    // however, it has to be present in the list of allowed cultures\n    // otherwise, it returns a default culture\n    // In addition, a missing header will result in the default culture returned as well\n    let c = serv.Globalization.RequestCulture\n    \n    serv.EndResponse c.Name\n```\n#### Request Logging\nServices for dealing with logging during the request handling.\n```F#\napp.Get \"/route\" \u003c- fun serv -\u003e\n    // Get ILogger for a category called \"MyLogCategory\"\n    let logger = serv.LoggerFor \"MyLogCategory\"\n    \n    // You can use the default and simplified logger\n    serv.Log.Information \"Info\"\n    serv.Log.Warning \"Warn\"\n    serv.Log.Error \"Error\"\n    serv.Log.Critical \"Crit\"\n    serv.Log.Debug \"Debug\"\n    serv.Log.Trace \"Trace\"\n    \n    serv.EndResponse ()\n```\n### DI Outside of Request\nIf your app requires an access to an inbuilt ASP Net Core service provider outside of the request handler (for example for your extension), then you can access the Service Provider from the app instance itself.\n```F#\n// Acquiring the DefaultServiceProvider wrapper\nlet serviceProvider = app.GetServiceProvider ()\n\n// It is wrapped in DefaultServiceProvider class\n// Raises MissingRequriedDependencyException on error\nlet s = serviceProvider.Required\u003cIRandomService\u003e ()\n// Returns None on missing dependency\nlet s = serviceProvider.Optional\u003cIRandomService\u003e ()\n// You can also provide a function returning a default implementation\n// if the service is missing for any reason\nlet s = serviceProvider.Get\u003cIRandomService\u003e ServiceConstructor\n\n// In case you need an access to raw IServiceProvider\nlet raw = serviceProvider.Raw\n```\n### Host Logging\nIf your app requires an access to a logger before the host is built, you can access a default one directly from your App instance.\n```F#\napp.Log.Information \"Info\"\napp.Log.Warning \"Warn\"\napp.Log.Error \"Error\"\napp.Log.Critical \"Crit\"\napp.Log.Debug \"Debug\"\napp.Log.Trace \"Trace\"\n```\n### System Configuration\n```F#\n// If you need to add or configure a service, or even to add a custom Endpoint\n\n// Generic system configuration types\ntype ServiceSetup = IWebHostEnvironment -\u003e IConfiguration -\u003e IServiceCollection -\u003e IServiceCollection\ntype AppSetup = IWebHostEnvironment -\u003e IConfiguration -\u003e IApplicationBuilder -\u003e IApplicationBuilder\ntype EndpointSetup = IEndpointRouteBuilder -\u003e IEndpointRouteBuilder\n\n// ServiceSetup injection points\napp.Services.BeforeServices\napp.Services.AfterServices\n// AppSetup injection points\napp.Services.BeforeApp\napp.Services.BeforeRouting\napp.Services.BeforeEndpoints\napp.Services.AfterEndpoints\napp.Services.AfterApp\n// EndpointSetup injection point\napp.Services.Endpoint\n\n// Configuration for inbuilt services\n\n// Sets the content root of the server\n// A write-only string property, default value - empty string\napp.Services.ContentRoot\n\n// Static files related configs\napp.Services.StaticFiles\n\n// Turning the static files middleware on and off\n// Off by default\napp.Services.StaticFiles.Enabled\n// Location of static files relatively to the content root\napp.Services.StaticFiles.WebRoot\n// Enables folder browsing for static files\napp.Services.StaticFiles.AllowBrowsing\n// Prefix for all static file routes\n// String property, default - empty string\napp.Services.StaticFiles.Route\n// If you need even more control, you can pass options directly\n// These classes are provided by ASP itself\n// None by default\napp.Services.StaticFiles.Options // Custom StaticFileOptions\napp.Services.StaticFiles.BrowsingOptions // Custom DirectoryBrowserOptions\n\n\ntype TemplateConfiguration = SystemDefaults -\u003e string -\u003e IServiceProvider -\u003e ITemplateRenderer\n\n// Templated related configs\napp.Services.Templating\n\n// Enable or disable the templating services\napp.Services.Templating.Enabled\n// The root location for all the templates relative to the Content Root\napp.Services.Templating.TemplateRoot\n// The default renderer, uses DotLiquid\napp.Services.Templating.DefaultRenderer\n// The default rendrer can be replaced\n// TemplateConfiguration should return an obj that implements ITemplateRenderer from WebFrame.Templating\napp.Services.Templating.CustomConfiguration\n\n// Globalization related configs\napp.Services.Globalization\n\n// A default culture\napp.Services.Globalization.DefaultCulture\n// A list of allowed cultures\napp.Services.Globalization.AllowedCultures\n\n// Exceptions related configs\napp.Services.Exceptions\n\n// User Excpetions are excpetions that are processed in app.Error handlers\n\n// Boolean fields\n// true - show / false - hide\napp.Services.Exceptions.ShowUserExceptionsByDefault\napp.Services.Exceptions.ShowInputExceptionsByDefault\napp.Services.Exceptions.ShowServerExceptionsByDefault\n\n// Map\u003cstring, bool\u003e fields\n// Environment Name -\u003e true - show / false - hide\napp.Services.Exceptions.UserExceptionFilter\napp.Services.Exceptions.InputExceptionFilter\napp.Services.Exceptions.ServerExceptionFilter\n\n// Whenever an environment name cannot be found in the filter list\n// it will use the default value\n```\n### Request Helpers\n```F#\nopen type WebFrame.Endpoints.Helpers\n\nlet app = App ()\n\n// This will always return a given message\napp.Get \"/\" \u003c- always \"Hello World!\"\n// Or a HttpWorkload\napp.Get \"/\" \u003c- always EndResponse\n// Or a lambda\napp.Get \"/\" \u003c- always ( fun () -\u003e EndResponse )\n\n// And the same as above but as a task\napp.GetTask \"/\" \u003c- alwaysTask \"Hello World!\"\napp.GetTask \"/\" \u003c- alwaysTask EndResponse\napp.GetTask \"/\" \u003c- alwaysTask ( fun () -\u003e task { return EndResponse } )\n\n// Returns an html page from the content root\napp.Get \"/\" \u003c- page \"Index.html\"\n// Or any other file\n// Content-Type will be selected automatically\napp.Get \"/\" \u003c- file \"Image.jpeg\"\n// Or manually \napp.Get \"/\" \u003c- file ( \"Image.jpeg\", \"text/plain\" )\n```\n### Modules\n```F#\nopen WebFrame\n\n// Creating a module with an \"/api\" path prefix\nlet api = AppModule \"/api\"\napi.Get \"/\" \u003c- fun serv -\u003e serv.EndResponse \"Api\"\n\nlet app = App ()\napp.Get \"/\" \u003c- fun serv -\u003e serv.EndResponse \"Main\"\n\n// Giving a name to a module and registering it with an app\napp.Module \"api\" \u003c- api\n\n// Routes:\n// \"/\" -\u003e Main\n// \"/api/\" -\u003e Api\n\napp.Run ()\n\n```\n### Testing\nHere is an example of who one should approach testing in WebFrame.\nThe key thing to remember is to use `app.TestServer ()` method instead of `app.Run ()` method.\n```F#\nopen System.Threading.Tasks\n\nopen Microsoft.AspNetCore.TestHost\n\nopen NUnit.Framework\n\nopen WebFrame\n\n// Standard app setup\nlet app = App ()\napp.Get \"/\" \u003c- fun serv -\u003e serv.EndResponse \"index\"\n\n[\u003cTest\u003e]\nlet ``Verifies that the test server is alive`` () = task {\n    use! server = app.TestServer ()\n    use client = server.GetTestClient ()\n    \n    let! r = client.GetAsync \"/\"\n    let! content = r.Content.ReadAsStringAsync ()\n    \n    Assert.AreEqual ( r.StatusCode, HttpStatusCode.OK )\n    Assert.AreEqual ( content, \"Hello World!\" )\n}\n```\n### Exceptions\nThere are 2 base Exceptions\n```F#\ntype InputException ( msg: string ) = inherit Exception ( msg )\ntype ServerException ( msg: string ) = inherit Exception ( msg )\n```\n`InputException` and its subclasses result in 4xx errors.\n\n`ServerException` and its subclasses result in 5xx errors.\n\nThese exceptions are automatically processed. If you want to handle a custom exception automatically, you can use:\n```F#\nopen WebFrame\n\n// Sample Exception\ntype CoffeeException () = inherit Exception \"I am a teapot!\"\ntype NotEnoughCoffeeException () = inherit Exception \"We need more coffee!\"\ntype TooMuchCoffeeException () = inherit Exception \"We've had enough coffee already!\"\n\nlet app = App ()\n\napp.Get \"/coffee\" \u003c- fun serv -\u003e\n    raise ( CoffeeException () )\n    serv.EndResponse ()\n        \napp.Get \"/work\" \u003c- fun serv -\u003e\n    raise ( NotEnoughCoffeeException () )\n    serv.EndResponse ()\n    \napp.Get \"/double-shot/coffee\" \u003c- fun serv -\u003e\n    raise ( TooMuchCoffeeException () )\n    serv.EndResponse ()\n\n// By default, user handled excpetions are shown in every environment\n// If you need to change this behaviour, please look at the Exceptions part of the System Configuration section\n\n// This code will automatically return 418 response code whenever CoffeeException is raised in the module/app\napp.Errors \u003c- Error.codeFor\u003cCoffeeException\u003e 418\n\n// If you need a custom processing, you can add your own handler\napp.Errors \u003c- Error.on \u003c| fun ( e: NotEnoughCoffeeException ) serv -\u003e\n    serv.StatusCode \u003c- 400\n    serv.EndResponse $\"{e.Message}\"\n\napp.Errors \u003c- Error.onTask \u003c| fun ( e: TooMuchCoffeeException ) serv -\u003e task {\n    serv.StatusCode \u003c- 500\n    return serv.EndResponse $\"{e.Message}\"\n}\n```\n## Changelog\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frussbaz%2Fwebframe","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frussbaz%2Fwebframe","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frussbaz%2Fwebframe/lists"}