├── docs ├── vscode.png ├── my-links.png ├── azure-portal.png └── code-structure.png ├── UrlShortener ├── db │ ├── migrations │ │ ├── V4__Add_Redirection_VisitCount.sql │ │ ├── V1__Create_Redirection.sql │ │ ├── V3__Add_Redirection_CreatorId.sql │ │ └── V2__Create_User.sql │ └── urlshortener.schema.json ├── wsconfig.json ├── paket.references ├── appsettings.json ├── wwwroot │ └── Content │ │ └── index.css ├── Startup.fs ├── Remoting.fs ├── UrlShortener.fsproj ├── Main.html ├── DataModel.fs ├── Authentication.fs ├── Site.fs ├── Database.fs └── Client.fs ├── .gitignore ├── .config └── dotnet-tools.json ├── paket.dependencies ├── UrlShortener.sln ├── .vscode ├── launch.json └── tasks.json ├── README.md ├── paket.lock └── .paket └── Paket.Restore.targets /docs/vscode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/websharper-samples/UrlShortener/HEAD/docs/vscode.png -------------------------------------------------------------------------------- /docs/my-links.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/websharper-samples/UrlShortener/HEAD/docs/my-links.png -------------------------------------------------------------------------------- /docs/azure-portal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/websharper-samples/UrlShortener/HEAD/docs/azure-portal.png -------------------------------------------------------------------------------- /docs/code-structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/websharper-samples/UrlShortener/HEAD/docs/code-structure.png -------------------------------------------------------------------------------- /UrlShortener/db/migrations/V4__Add_Redirection_VisitCount.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE `redirection` 2 | ADD COLUMN `visitCount` INTEGER NOT NULL DEFAULT 0; -------------------------------------------------------------------------------- /UrlShortener/db/migrations/V1__Create_Redirection.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `redirection` ( 2 | `id` INTEGER NOT NULL PRIMARY KEY, 3 | `url` TEXT NOT NULL 4 | ); -------------------------------------------------------------------------------- /UrlShortener/wsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://websharper.com/wsconfig.schema.json", 3 | "project": "site", 4 | "outputDir": "wwwroot" 5 | } 6 | -------------------------------------------------------------------------------- /UrlShortener/db/migrations/V3__Add_Redirection_CreatorId.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE `redirection` 2 | ADD COLUMN `creatorId` GUID NOT NULL REFERENCES `user` (`id`) DEFAULT 0; -------------------------------------------------------------------------------- /UrlShortener/paket.references: -------------------------------------------------------------------------------- 1 | WebSharper.FSharp 2 | WebSharper.AspNetCore 3 | WebSharper.Mvu 4 | WebSharper.OAuth 5 | SQLProvider 6 | System.Data.SQLite.Core 7 | Evolve -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | obj/ 3 | WebSharper/ 4 | paket-files/ 5 | *.user 6 | .vs/ 7 | launchSettings.json 8 | appsettings.*.json 9 | packages/ 10 | *.db 11 | lib/ 12 | -------------------------------------------------------------------------------- /UrlShortener/db/migrations/V2__Create_User.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `user` ( 2 | `id` GUID NOT NULL PRIMARY KEY, 3 | `facebookId` TEXT NOT NULL UNIQUE, 4 | `fullName` TEXT NOT NULL 5 | ); -------------------------------------------------------------------------------- /.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "paket": { 6 | "version": "5.257.0", 7 | "commands": [ 8 | "paket" 9 | ] 10 | }, 11 | "fake-cli": { 12 | "version": "5.20.4-alpha.1642", 13 | "commands": [ 14 | "fake" 15 | ] 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /UrlShortener/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "websharper": { 3 | "WebSharper.JQuery.Resources.JQuery": "https://code.jquery.com/jquery-3.2.1.min.js" 4 | }, 5 | "ConnectionStrings": { 6 | "UrlShortener": "Data Source=db/urlshortener.db" 7 | }, 8 | "facebook":{ 9 | "clientId": "REPLACE_ME", 10 | "clientSecret": "REPLACE_ME" 11 | } 12 | } -------------------------------------------------------------------------------- /paket.dependencies: -------------------------------------------------------------------------------- 1 | source https://api.nuget.org/v3/index.json 2 | 3 | framework: netcoreapp3.1 4 | storage: none 5 | 6 | nuget WebSharper.AspNetCore 7 | nuget WebSharper.FSharp 8 | nuget WebSharper.Mvu 9 | nuget WebSharper.OAuth 10 | nuget SQLProvider 1.1.76 11 | nuget System.Data.SQLite.Core 1.0.112 storage: packages 12 | nuget Evolve ~> 1.8 -------------------------------------------------------------------------------- /UrlShortener/wwwroot/Content/index.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | font-family: 'Open Sans', serif; 3 | font-size: 14px; 4 | font-weight: 300; 5 | } 6 | 7 | .hero.is-success { 8 | background: #F2F6FA; 9 | } 10 | 11 | .hero .nav, .hero.is-success .nav { 12 | -webkit-box-shadow: none; 13 | box-shadow: none; 14 | } 15 | 16 | .login-page input { 17 | font-weight: 300; 18 | } 19 | 20 | .login-page p { 21 | font-weight: 700; 22 | } 23 | 24 | .login-page p.subtitle { 25 | padding-top: 1rem; 26 | } 27 | 28 | .table th, .table td { 29 | vertical-align: middle; 30 | } 31 | -------------------------------------------------------------------------------- /UrlShortener/Startup.fs: -------------------------------------------------------------------------------- 1 | namespace UrlShortener 2 | 3 | open Microsoft.AspNetCore 4 | open Microsoft.AspNetCore.Builder 5 | open Microsoft.AspNetCore.Hosting 6 | open Microsoft.AspNetCore.Http 7 | open Microsoft.Extensions.DependencyInjection 8 | open WebSharper.AspNetCore 9 | 10 | type Startup() = 11 | 12 | member this.ConfigureServices(services: IServiceCollection) = 13 | services.AddSitelet() 14 | .AddTransient() 15 | .AddAuthentication("WebSharper") 16 | .AddCookie("WebSharper", fun options -> ()) 17 | |> ignore 18 | 19 | member this.Configure(app: IApplicationBuilder, env: IHostingEnvironment, db: Database.Context) = 20 | if env.IsDevelopment() then app.UseDeveloperExceptionPage() |> ignore 21 | 22 | db.Migrate() 23 | 24 | app.UseAuthentication() 25 | .UseStaticFiles() 26 | .UseWebSharper() 27 | .Run(fun context -> 28 | context.Response.StatusCode <- 404 29 | context.Response.WriteAsync("Page not found")) 30 | 31 | module Program = 32 | 33 | [] 34 | let main args = 35 | WebHost 36 | .CreateDefaultBuilder(args) 37 | .UseStartup() 38 | .Build() 39 | .Run() 40 | 0 41 | -------------------------------------------------------------------------------- /UrlShortener/Remoting.fs: -------------------------------------------------------------------------------- 1 | namespace UrlShortener 2 | 3 | open System 4 | open WebSharper 5 | open UrlShortener.DataModel 6 | open UrlShortener.Database 7 | 8 | /// Functions that can be called from the client side. 9 | module Remoting = 10 | 11 | /// Retrieve a list of the links created by the logged in user. 12 | [] 13 | let GetMyLinks () : Async = 14 | let ctx = Web.Remoting.GetContext() 15 | async { 16 | match! Authentication.GetLoggedInUserId ctx with 17 | | None -> return Array.empty 18 | | Some userId -> return! ctx.Db.GetAllUserLinks(userId, ctx) 19 | } 20 | 21 | /// Delete this link, if it was created by the logged in user. 22 | /// Return a list of the links created by the logged in user. 23 | [] 24 | let DeleteLink (slug: Link.Slug) : Async = 25 | let ctx = Web.Remoting.GetContext() 26 | async { 27 | match! Authentication.GetLoggedInUserId ctx with 28 | | None -> return Array.empty 29 | | Some userId -> 30 | do! ctx.Db.DeleteLink(userId, slug) 31 | return! ctx.Db.GetAllUserLinks(userId, ctx) 32 | } 33 | 34 | /// Create a new link pointing to this URL. 35 | /// Return a list of the links created by the logged in user. 36 | [] 37 | let CreateNewLink (url: Link.Slug) : Async = 38 | let ctx = Web.Remoting.GetContext() 39 | async { 40 | match! Authentication.GetLoggedInUserId ctx with 41 | | None -> return Array.empty 42 | | Some userId -> 43 | let! _ = ctx.Db.CreateLink(userId, url) 44 | return! ctx.Db.GetAllUserLinks(userId, ctx) 45 | } 46 | -------------------------------------------------------------------------------- /UrlShortener.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26124.0 5 | MinimumVisualStudioVersion = 15.0.26124.0 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".paket", ".paket", "{66732F6D-A8B3-4F20-8695-93C9A2D67E16}" 7 | ProjectSection(SolutionItems) = preProject 8 | paket.dependencies = paket.dependencies 9 | EndProjectSection 10 | EndProject 11 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "UrlShortener", "UrlShortener\UrlShortener.fsproj", "{67BFD509-2604-4572-A499-4E2E8578BA02}" 12 | EndProject 13 | Global 14 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 15 | Debug|Any CPU = Debug|Any CPU 16 | Debug|x64 = Debug|x64 17 | Debug|x86 = Debug|x86 18 | Release|Any CPU = Release|Any CPU 19 | Release|x64 = Release|x64 20 | Release|x86 = Release|x86 21 | EndGlobalSection 22 | GlobalSection(SolutionProperties) = preSolution 23 | HideSolutionNode = FALSE 24 | EndGlobalSection 25 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 26 | {67BFD509-2604-4572-A499-4E2E8578BA02}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {67BFD509-2604-4572-A499-4E2E8578BA02}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {67BFD509-2604-4572-A499-4E2E8578BA02}.Debug|x64.ActiveCfg = Debug|Any CPU 29 | {67BFD509-2604-4572-A499-4E2E8578BA02}.Debug|x64.Build.0 = Debug|Any CPU 30 | {67BFD509-2604-4572-A499-4E2E8578BA02}.Debug|x86.ActiveCfg = Debug|Any CPU 31 | {67BFD509-2604-4572-A499-4E2E8578BA02}.Debug|x86.Build.0 = Debug|Any CPU 32 | {67BFD509-2604-4572-A499-4E2E8578BA02}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {67BFD509-2604-4572-A499-4E2E8578BA02}.Release|Any CPU.Build.0 = Release|Any CPU 34 | {67BFD509-2604-4572-A499-4E2E8578BA02}.Release|x64.ActiveCfg = Release|Any CPU 35 | {67BFD509-2604-4572-A499-4E2E8578BA02}.Release|x64.Build.0 = Release|Any CPU 36 | {67BFD509-2604-4572-A499-4E2E8578BA02}.Release|x86.ActiveCfg = Release|Any CPU 37 | {67BFD509-2604-4572-A499-4E2E8578BA02}.Release|x86.Build.0 = Release|Any CPU 38 | EndGlobalSection 39 | EndGlobal 40 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Launch (console)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | "program": "${workspaceFolder}/UrlShortener/bin/Debug/netcoreapp2.1/UrlShortener.dll", 13 | "args": [], 14 | "cwd": "${workspaceFolder}/UrlShortener", 15 | "console": "internalConsole", 16 | "stopAtEntry": false, 17 | "internalConsoleOptions": "openOnSessionStart" 18 | }, 19 | { 20 | "name": ".NET Core Launch (web)", 21 | "type": "coreclr", 22 | "request": "launch", 23 | "preLaunchTask": "build", 24 | "program": "${workspaceFolder}/UrlShortener/bin/Debug/netcoreapp2.1/UrlShortener.dll", 25 | "args": [], 26 | "cwd": "${workspaceFolder}/UrlShortener", 27 | "stopAtEntry": false, 28 | "internalConsoleOptions": "openOnSessionStart", 29 | "launchBrowser": { 30 | "enabled": true, 31 | "args": "${auto-detect-url}", 32 | "windows": { 33 | "command": "cmd.exe", 34 | "args": "/C start ${auto-detect-url}" 35 | }, 36 | "osx": { 37 | "command": "open" 38 | }, 39 | "linux": { 40 | "command": "xdg-open" 41 | } 42 | }, 43 | "env": { 44 | "ASPNETCORE_ENVIRONMENT": "Development" 45 | }, 46 | "sourceFileMap": { 47 | "/Views": "${workspaceFolder}/Views" 48 | } 49 | }, 50 | { 51 | "name": ".NET Core Attach", 52 | "type": "coreclr", 53 | "request": "attach", 54 | "processId": "${command:pickProcess}" 55 | } 56 | ] 57 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "build", 8 | "command": "dotnet build", 9 | "type": "shell", 10 | "group": "build", 11 | "problemMatcher": "$msCompile" 12 | }, 13 | { 14 | "label": "run", 15 | "command": "dotnet run -p UrlShortener", 16 | "options": { 17 | "env": { 18 | "ASPNETCORE_ENVIRONMENT": "development" 19 | } 20 | }, 21 | "linux": { 22 | "options": { 23 | "env": { 24 | "LD_LIBRARY_PATH": "${workspaceFolder}/packages/System.Data.SQLite.Core/runtimes/linux-x64/lib/netstandard2.0" 25 | } 26 | } 27 | }, 28 | "osx": { 29 | "options": { 30 | "env": { 31 | "LD_LIBRARY_PATH": "${workspaceFolder}/packages/System.Data.SQLite.Core/runtimes/osx-x64/lib/netstandard2.0" 32 | } 33 | } 34 | }, 35 | "type": "shell", 36 | "group": "build", 37 | "problemMatcher": "$msCompile" 38 | }, 39 | { 40 | "label": "watch", 41 | "command": "dotnet watch -p UrlShortener.sln run -p UrlShortener", 42 | "options": { 43 | "env": { 44 | "ASPNETCORE_ENVIRONMENT": "development" 45 | } 46 | }, 47 | "linux": { 48 | "options": { 49 | "env": { 50 | "LD_LIBRARY_PATH": "${workspaceFolder}/packages/System.Data.SQLite.Core/runtimes/linux-x64/lib/netstandard2.0" 51 | } 52 | } 53 | }, 54 | "osx": { 55 | "options": { 56 | "env": { 57 | "LD_LIBRARY_PATH": "${workspaceFolder}/packages/System.Data.SQLite.Core/runtimes/osx-x64/lib/netstandard2.0" 58 | } 59 | } 60 | }, 61 | "type": "shell", 62 | "group": "build", 63 | "problemMatcher": "$msCompile" 64 | } 65 | ] 66 | } -------------------------------------------------------------------------------- /UrlShortener/UrlShortener.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | Website 6 | $(MSBuildThisFileDirectory)/wwwroot 7 | True 8 | true 9 | 10 | 11 | x64 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | x64/SQLite.Interop.dll 26 | PreserveNewest 27 | 28 | 29 | x86/SQLite.Interop.dll 30 | PreserveNewest 31 | 32 | 33 | 34 | 37 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /UrlShortener/Main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ${Title} 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 | 17 | 18 | 42 | 43 | 61 | 62 |
63 |
64 |
65 |
66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /UrlShortener/DataModel.fs: -------------------------------------------------------------------------------- 1 | /// Core data model types and functionality. 2 | module UrlShortener.DataModel 3 | 4 | open System 5 | open WebSharper 6 | open WebSharper.Sitelets.InferRouter 7 | 8 | /// Defines all the endpoints served by this application. 9 | type EndPoint = 10 | | [] Home 11 | | [] MyLinks 12 | | [] Logout 13 | | [] OAuth 14 | | [] Link of slug: string 15 | | [] CreateLink of url: string 16 | 17 | /// Enables creating URLs from EndPoints and vice-versa. 18 | let Router = Router.Infer() 19 | 20 | /// Functionality regarding redirection links. 21 | module Link = 22 | 23 | /// Link id as stored in the database. 24 | type Id = int64 25 | 26 | /// Link id as shown in URLs. 27 | type Slug = string 28 | 29 | /// Information about a redirection link. 30 | [] 31 | type Data = 32 | { 33 | Slug: Slug 34 | LinkUrl: string 35 | TargetUrl: string 36 | VisitCount: int64 37 | } 38 | 39 | /// Create a link slug from a link id. 40 | /// Uses base64-URL encoding. 41 | let EncodeLinkId (linkId: Id) : Slug = 42 | let bytes = BitConverter.GetBytes(linkId) 43 | Convert.ToBase64String(bytes) 44 | .Replace("=", "") 45 | .Replace('+', '-') 46 | .Replace('/', '_') 47 | 48 | /// Extract the link id from a link slug. 49 | /// Uses base64-URL decoding. 50 | let TryDecodeLinkId (slug: Slug) : Id option = 51 | if String.IsNullOrEmpty slug then 52 | Some 0L 53 | else 54 | let s = 55 | slug.Replace('-', '+') 56 | .Replace('_', '/') 57 | + (String.replicate (4 - slug.Length % 4) "=") 58 | try BitConverter.ToInt64(Convert.FromBase64String s, 0) |> Some 59 | with _ -> None 60 | 61 | /// Create a new random link id. 62 | let NewLinkId() : Id = 63 | let bytes = Array.zeroCreate 8 64 | Random().NextBytes(bytes) 65 | BitConverter.ToInt64(bytes, 0) 66 | 67 | /// Create a full URL from a link slug. 68 | let SlugToFullUrl (ctx: Web.Context) (slug: Slug) : string = 69 | let builder = UriBuilder(ctx.RequestUri) 70 | builder.Path <- Router.Link(Link slug) 71 | builder.Uri.ToString() 72 | 73 | /// Functionality regarding application users. 74 | module User = 75 | 76 | /// User ids are GUIDs both in the database and in user code. 77 | type Id = Guid 78 | 79 | /// Information about a user. 80 | [] 81 | type Data = 82 | { 83 | UserId: Id 84 | FullName: string 85 | } 86 | 87 | /// Create a new random user id. 88 | let NewUserId() : Id = 89 | Guid.NewGuid() 90 | -------------------------------------------------------------------------------- /UrlShortener/db/urlshortener.schema.json: -------------------------------------------------------------------------------- 1 | {"Columns@":[{"Key":"main.user","Value":{"serializedData":[{"key":"facebookId","value":{"HasDefault@":false,"IsAutonumber@":false,"IsNullable@":false,"IsPrimaryKey@":false,"Name@":"facebookId","TypeInfo@":{"value":"text"},"TypeMapping@":{"ClrType@":"System.String","DbType@":16,"ProviderTypeName@":{"value":"text"},"ProviderType@":{"value":16}}}},{"key":"fullName","value":{"HasDefault@":false,"IsAutonumber@":false,"IsNullable@":false,"IsPrimaryKey@":false,"Name@":"fullName","TypeInfo@":{"value":"text"},"TypeMapping@":{"ClrType@":"System.String","DbType@":16,"ProviderTypeName@":{"value":"text"},"ProviderType@":{"value":16}}}},{"key":"id","value":{"HasDefault@":false,"IsAutonumber@":true,"IsNullable@":false,"IsPrimaryKey@":true,"Name@":"id","TypeInfo@":{"value":"guid"},"TypeMapping@":{"ClrType@":"System.Guid","DbType@":9,"ProviderTypeName@":{"value":"guid"},"ProviderType@":{"value":9}}}}]}},{"Key":"main.redirection","Value":{"serializedData":[{"key":"creatorId","value":{"HasDefault@":true,"IsAutonumber@":false,"IsNullable@":false,"IsPrimaryKey@":false,"Name@":"creatorId","TypeInfo@":{"value":"guid"},"TypeMapping@":{"ClrType@":"System.Guid","DbType@":9,"ProviderTypeName@":{"value":"guid"},"ProviderType@":{"value":9}}}},{"key":"id","value":{"HasDefault@":false,"IsAutonumber@":true,"IsNullable@":false,"IsPrimaryKey@":true,"Name@":"id","TypeInfo@":{"value":"integer"},"TypeMapping@":{"ClrType@":"System.Int64","DbType@":12,"ProviderTypeName@":{"value":"integer"},"ProviderType@":{"value":12}}}},{"key":"url","value":{"HasDefault@":false,"IsAutonumber@":false,"IsNullable@":false,"IsPrimaryKey@":false,"Name@":"url","TypeInfo@":{"value":"text"},"TypeMapping@":{"ClrType@":"System.String","DbType@":16,"ProviderTypeName@":{"value":"text"},"ProviderType@":{"value":16}}}},{"key":"visitCount","value":{"HasDefault@":true,"IsAutonumber@":false,"IsNullable@":false,"IsPrimaryKey@":false,"Name@":"visitCount","TypeInfo@":{"value":"integer"},"TypeMapping@":{"ClrType@":"System.Int64","DbType@":12,"ProviderTypeName@":{"value":"integer"},"ProviderType@":{"value":12}}}}]}}],"Individuals@":[],"IsOffline@":true,"Packages@":[],"PrimaryKeys@":[{"Key":"main.user","Value":{"head":"id","tail":{"head":null,"tail":null}}},{"Key":"main.redirection","Value":{"head":"id","tail":{"head":null,"tail":null}}}],"Relationships@":[{"Key":"main.user","Value":{"m_Item1":{"head":{"ForeignKey@":"creatorId","ForeignTable@":"main.redirection","Name@":"FK_redirection_0_0","PrimaryKey@":"id","PrimaryTable@":"main.user"},"tail":{"head":null,"tail":null}},"m_Item2":{"head":null,"tail":null}}},{"Key":"main.redirection","Value":{"m_Item1":{"head":null,"tail":null},"m_Item2":{"head":{"ForeignKey@":"creatorId","ForeignTable@":"main.redirection","Name@":"FK_redirection_0_0","PrimaryKey@":"id","PrimaryTable@":"main.user"},"tail":{"head":null,"tail":null}}}}],"SprocsParams@":[],"Sprocs@":[{"__type":"Sproc.Root:#FSharp.Data.Sql.Schema","_tag":0,"item1":"Pragma","item2":{"__type":"Sproc.Sproc:#FSharp.Data.Sql.Schema","_tag":2,"item":{"Name@":{"Owner@":"Main","PackageName@":"","ProcName@":"Get"}}}},{"__type":"Sproc.Root:#FSharp.Data.Sql.Schema","_tag":0,"item1":"Pragma","item2":{"__type":"Sproc.Sproc:#FSharp.Data.Sql.Schema","_tag":2,"item":{"Name@":{"Owner@":"Main","PackageName@":"","ProcName@":"GetOf"}}}}],"Tables@":[{"Key":"main.user","Value":{"Name@":"user","Schema@":"main","Type@":"table"}},{"Key":"main.redirection","Value":{"Name@":"redirection","Schema@":"main","Type@":"table"}},{"Key":"main.changelog","Value":{"Name@":"changelog","Schema@":"main","Type@":"table"}}]} -------------------------------------------------------------------------------- /UrlShortener/Authentication.fs: -------------------------------------------------------------------------------- 1 | module UrlShortener.Authentication 2 | 3 | open System 4 | open System.IO 5 | open System.Net 6 | open WebSharper 7 | open WebSharper.Sitelets 8 | open WebSharper.OAuth.OAuth2 9 | open Microsoft.Extensions.Configuration 10 | open UrlShortener.DataModel 11 | open UrlShortener.Database 12 | 13 | /// The JSON returned by Facebook when querying user data. 14 | type private FacebookUserData = { id: string; name: string } 15 | 16 | /// Get the user data for the owner of this token. 17 | let private getFacebookUserData (token: AuthenticationToken) : Async = 18 | async { 19 | let req = WebRequest.CreateHttp("https://graph.facebook.com/v3.1/me") 20 | token.AuthorizeRequest req 21 | let! resp = req.AsyncGetResponse() 22 | use r = new StreamReader(resp.GetResponseStream()) 23 | let! body = r.ReadToEndAsync() |> Async.AwaitTask 24 | return WebSharper.Json.Deserialize(body) 25 | } 26 | 27 | /// The OAuth2 client for Facebook login. 28 | let FacebookProvider (config: IConfiguration) = 29 | let config = config.GetSection("facebook") 30 | Provider.Setup( 31 | // Create a Facebook OAuth client with the given app credentials. 32 | service = ServiceSettings.Facebook(config.["clientId"], config.["clientSecret"]), 33 | // Upon success or failure, users are redirected to EndPoint.OAuth (which points to to "/oauth"). 34 | redirectEndpointAction = OAuth, 35 | redirectEndpoint = (fun ctx resp -> 36 | match resp with 37 | | AuthenticationResponse.Success token -> 38 | // On successful Facebook login: 39 | async { 40 | // 1. Query Facebook for user data (id, full name); 41 | let! fbUserData = getFacebookUserData token 42 | // 2. Match it with a user in our database, or create one; 43 | let! userId = ctx.Db.GetOrCreateFacebookUser(fbUserData.id, fbUserData.name) 44 | // 3. Log the user in; 45 | do! ctx.UserSession.LoginUser(userId.ToString("N")) 46 | // 4. Redirect them to the home page. 47 | return! Content.RedirectTemporary Home 48 | } 49 | | AuthenticationResponse.Error e -> 50 | // On failure, show an error message. 51 | e.Description 52 | |> Option.defaultValue "Failed to log in with Facebook" 53 | |> Content.Text 54 | |> Content.SetStatus Http.Status.InternalServerError 55 | | AuthenticationResponse.ImplicitSuccess -> 56 | // Implicit login is used for client-only applications, we don't use it. 57 | Content.Text "This application doesn't use implicit login" 58 | |> Content.SetStatus Http.Status.NotImplemented 59 | ) 60 | ) 61 | 62 | /// Get the user id of the currently logged in user. 63 | let GetLoggedInUserId (ctx: Web.Context) : Async = async { 64 | match! ctx.UserSession.GetLoggedInUser() with 65 | | None -> return None 66 | | Some s -> 67 | match Guid.TryParse(s) with 68 | | true, id -> return Some id 69 | | false, _ -> 70 | do! ctx.UserSession.Logout() 71 | return None 72 | } 73 | 74 | /// Get the user data of the currently logged in user. 75 | let GetLoggedInUserData (ctx: Web.Context) : Async = async { 76 | match! GetLoggedInUserId ctx with 77 | | None -> return None 78 | | Some uid -> return! ctx.Db.GetUserData(uid) 79 | } -------------------------------------------------------------------------------- /UrlShortener/Site.fs: -------------------------------------------------------------------------------- 1 | namespace UrlShortener 2 | 3 | open System 4 | open WebSharper 5 | open WebSharper.AspNetCore 6 | open WebSharper.Sitelets 7 | open WebSharper.OAuth 8 | open WebSharper.UI 9 | open WebSharper.UI.Html 10 | open WebSharper.UI.Server 11 | open Microsoft.Extensions.Configuration 12 | open UrlShortener.DataModel 13 | open UrlShortener.Database 14 | 15 | type MainTemplate = Templating.Template<"Main.html", serverLoad = Templating.ServerLoad.WhenChanged> 16 | 17 | /// The full website. 18 | type Site(config: IConfiguration) = 19 | inherit SiteletService() 20 | 21 | let NavBar (ctx: Context) = 22 | MainTemplate.MainNavBar() 23 | .HomeUrl(ctx.Link Home) 24 | .MyLinksUrl(ctx.Link MyLinks) 25 | .LogoutUrl(ctx.Link Logout) 26 | .Doc() 27 | 28 | /// The page layout used by all our HTML content. 29 | let Page (ctx: Context) (withNavBar: bool) (body: Doc) = 30 | Content.Page( 31 | MainTemplate() 32 | .Body(body) 33 | .NavBar(if withNavBar then NavBar ctx else Doc.Empty) 34 | .Doc() 35 | ) 36 | 37 | /// The page body layout for "splash" style pages: home, login, link created. 38 | let SplashBody (title: string) (subtitle: string option) (content: Doc) = 39 | MainTemplate.SplashPage() 40 | .Title(title) 41 | .Subtitle(defaultArg subtitle "") 42 | .Content(content) 43 | .Doc() 44 | 45 | /// Content for the login page. 46 | let LoginPage (ctx: Context) (facebook: OAuth2.Provider) = 47 | a [ 48 | attr.href (facebook.GetAuthorizationRequestUrl(ctx)) 49 | attr.``class`` "button is-block is-info is-large is-fullwidth" 50 | ] [ 51 | text "Log in with Facebook" 52 | ] 53 | |> SplashBody "WebSharper URL Shortener" (Some "Please login to proceed.") 54 | |> Page ctx false 55 | 56 | /// Content for the home page once logged in. 57 | let HomePage (ctx: Context) (user: User.Data) = 58 | MainTemplate.HomeContent() 59 | .CreateLinkUrl(ctx.Link (CreateLink "")) 60 | .Doc() 61 | |> SplashBody "WebSharper URL Shortener" None 62 | |> Page ctx true 63 | 64 | /// Content for the account management page. 65 | let MyLinksPage (ctx: Context) (user: User.Data) = 66 | MainTemplate.MyLinksPage() 67 | .Content(client <@ Client.MyLinks user @>) 68 | .Doc() 69 | |> Page ctx true 70 | 71 | /// Content shown after successfully creating a link. 72 | let LinkCreatedPage (ctx: Context) (slug: Link.Slug) = 73 | let url = Link.SlugToFullUrl ctx slug 74 | div [attr.``class`` "title"] [ 75 | a [ 76 | attr.href url 77 | attr.target "_blank" 78 | attr.``class`` "button is-large is-success" 79 | ] [ 80 | text url 81 | ] 82 | ] 83 | |> SplashBody "Link created!" None 84 | |> Page ctx true 85 | 86 | /// Content when a link is not found 87 | let LinkNotFoundPage (ctx: Context) = 88 | div [attr.``class`` "subtitle has-text-dark"] [ 89 | text "This link does not exist or has been deleted." 90 | ] 91 | |> SplashBody "Not found!" None 92 | |> Page ctx false 93 | 94 | let facebook = Authentication.FacebookProvider config 95 | 96 | override val Sitelet = 97 | facebook.RedirectEndpointSitelet 98 | <|> 99 | Application.MultiPage (fun (ctx: Context) endpoint -> async { 100 | match endpoint with 101 | | Home -> 102 | match! Authentication.GetLoggedInUserData ctx with 103 | | None -> return! LoginPage ctx facebook 104 | | Some user -> return! HomePage ctx user 105 | | MyLinks -> 106 | match! Authentication.GetLoggedInUserData ctx with 107 | | None -> return! Content.RedirectTemporary Home 108 | | Some user -> return! MyLinksPage ctx user 109 | | Link slug -> 110 | match! ctx.Db.TryVisitLink(slug) with 111 | | Some url -> return! Content.RedirectTemporaryToUrl url 112 | | None -> return! LinkNotFoundPage ctx 113 | | Logout -> 114 | do! ctx.UserSession.Logout() 115 | return! Content.RedirectTemporary Home 116 | | CreateLink url -> 117 | match! Authentication.GetLoggedInUserId ctx with 118 | | None -> return! Content.RedirectTemporary Home 119 | | Some uid -> 120 | let! slug = ctx.Db.CreateLink(uid, url) 121 | return! LinkCreatedPage ctx slug 122 | | OAuth -> 123 | // This is already handled by facebook.RedirectEndpointSitelet. 124 | return! Content.ServerError 125 | }) 126 | -------------------------------------------------------------------------------- /UrlShortener/Database.fs: -------------------------------------------------------------------------------- 1 | module UrlShortener.Database 2 | 3 | open System 4 | open FSharp.Data.Sql 5 | open FSharp.Data.Sql.Transactions 6 | open Microsoft.Extensions.Configuration 7 | open Microsoft.Extensions.Logging 8 | open Microsoft.Extensions.DependencyInjection 9 | open WebSharper 10 | open WebSharper.AspNetCore 11 | open UrlShortener.DataModel 12 | 13 | type Sql = SqlDataProvider< 14 | // Connect to SQLite using System.Data.Sqlite. 15 | Common.DatabaseProviderTypes.SQLITE, 16 | SQLiteLibrary = Common.SQLiteLibrary.SystemDataSQLite, 17 | ResolutionPath = const(__SOURCE_DIRECTORY__ + "/../packages/System.Data.SQLite.Core/lib/netstandard2.0/"), 18 | // Store the database file in db/urlshortener.db. 19 | ConnectionString = const("Data Source=" + __SOURCE_DIRECTORY__ + "/db/urlshortener.db"), 20 | // Store the schema as JSON so that the compiler doesn't need the database to exist. 21 | ContextSchemaPath = const(__SOURCE_DIRECTORY__ + "/db/urlshortener.schema.json"), 22 | UseOptionTypes = true> 23 | 24 | /// ASP.NET Core service that creates a new data context every time it's required. 25 | type Context(config: IConfiguration, logger: ILogger) = 26 | do logger.LogInformation("Creating db context") 27 | 28 | let db = 29 | let connString = config.GetSection("ConnectionStrings").["UrlShortener"] 30 | Sql.GetDataContext(connString, TransactionOptions.Default) 31 | 32 | /// Apply all migrations. 33 | member this.Migrate() = 34 | try 35 | use ctx = db.CreateConnection() 36 | let evolve = 37 | new Evolve.Evolve(ctx, logger.LogInformation, 38 | Locations = ["db/migrations"], 39 | IsEraseDisabled = true) 40 | evolve.Migrate() 41 | with ex -> 42 | logger.LogCritical("Database migration failed: {0}", ex) 43 | 44 | /// Get the user for this Facebook user id, or create a user if there isn't one. 45 | member this.GetOrCreateFacebookUser(fbUserId: string, fbUserName: string) : Async = async { 46 | let existing = 47 | query { for u in db.Main.User do 48 | where (u.FacebookId = fbUserId) 49 | select (Some u.Id) 50 | headOrDefault } 51 | match existing with 52 | | None -> 53 | let u = 54 | db.Main.User.Create( 55 | Id = User.NewUserId(), 56 | FacebookId = fbUserId, 57 | FullName = fbUserName) 58 | do! db.SubmitUpdatesAsync() 59 | return u.Id 60 | | Some id -> 61 | return id 62 | } 63 | 64 | /// Get the user's full name. 65 | member this.GetUserData(userId: User.Id) : Async = async { 66 | let name = 67 | query { for u in db.Main.User do 68 | where (u.Id = userId) 69 | select (Some u.FullName) 70 | headOrDefault } 71 | return name |> Option.map (fun name -> 72 | { 73 | UserId = userId 74 | FullName = name 75 | } : User.Data 76 | ) 77 | } 78 | 79 | /// Create a new link on this user's behalf, pointing to this url. 80 | /// Returns the slug for this new link. 81 | member this.CreateLink(userId: User.Id, url: string) : Async = async { 82 | let r = 83 | db.Main.Redirection.Create( 84 | Id = Link.NewLinkId(), 85 | CreatorId = userId, 86 | Url = url) 87 | do! db.SubmitUpdatesAsync() 88 | return Link.EncodeLinkId r.Id 89 | } 90 | 91 | /// Get the url pointed to by the given slug, if any, 92 | /// and increment its visit count. 93 | member this.TryVisitLink(slug: Link.Slug) : Async = async { 94 | match Link.TryDecodeLinkId slug with 95 | | None -> return None 96 | | Some linkId -> 97 | let link = 98 | query { for l in db.Main.Redirection do 99 | where (l.Id = linkId) 100 | select (Some l) 101 | headOrDefault } 102 | match link with 103 | | None -> return None 104 | | Some link -> 105 | link.VisitCount <- link.VisitCount + 1L 106 | do! db.SubmitUpdatesAsync() 107 | return Some link.Url 108 | } 109 | 110 | /// Get data about all the links created by the given user. 111 | member this.GetAllUserLinks(userId: User.Id, ctx: Web.Context) : Async = async { 112 | let links = 113 | query { for l in db.Main.Redirection do 114 | where (l.CreatorId = userId) 115 | select l } 116 | return links 117 | |> Seq.map (fun l -> 118 | let slug = Link.EncodeLinkId l.Id 119 | let url = Link.SlugToFullUrl ctx slug 120 | { 121 | Slug = slug 122 | LinkUrl = url 123 | TargetUrl = l.Url 124 | VisitCount = l.VisitCount 125 | } : Link.Data 126 | ) 127 | |> Array.ofSeq 128 | } 129 | 130 | /// Check that this link belongs to this user, and if yes, delete it. 131 | member this.DeleteLink(userId: User.Id, slug: Link.Slug) : Async = async { 132 | match Link.TryDecodeLinkId slug with 133 | | None -> return () 134 | | Some linkId -> 135 | let link = 136 | query { for l in db.Main.Redirection do 137 | where (l.Id = linkId && l.CreatorId = userId) 138 | select (Some l) 139 | headOrDefault } 140 | match link with 141 | | None -> return () 142 | | Some l -> 143 | l.Delete() 144 | return! db.SubmitUpdatesAsync() 145 | } 146 | 147 | type Web.Context with 148 | /// Get a new database context. 149 | member this.Db : Context = 150 | this.HttpContext().RequestServices.GetRequiredService() 151 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WebSharper URL Shortener 2 | 3 | This project demonstrates how to use [WebSharper](https://websharper.com) to build the server side and client side of a web application. 4 | 5 | It features: 6 | * a server side using [WebSharper sitelets](https://developers.websharper.com/docs/v4.x/fs/sitelets) hosted on ASP.NET Core; 7 | * a SQLite database driven by [SQLProvider](https://fsprojects.github.io/SQLProvider/), with schema migrations using [Evolve](https://evolve-db.netlify.com/); 8 | * Facebook login using OAuth; 9 | * a [Model-View-Update](https://dotnet-websharper.github.io/mvu) client side page, using [WebSharper remoting](https://developers.websharper.com/docs/v4.x/fs/remoting) for server interaction; 10 | * reactive DOM construction, using both HTML templates and F# HTML functions. 11 | 12 | [![Application screenshot: "My links" page](./docs/my-links.png)](https://websharper-url-shortener.azurewebsites.net/) 13 | 14 | You can see it running on Azure [here](https://websharper-url-shortener.azurewebsites.net/). 15 | 16 | ## Requirements 17 | 18 | * [The .NET Core SDK](https://www.microsoft.com/net/download), version 2.1.401 or newer. 19 | * On Windows: .NET Framework 4.6.1 (select it in Visual Studio installer, or download the Dev Pack [here](https://www.microsoft.com/net/download/dotnet-framework/net461)). 20 | * On OSX or Linux: [Mono 5.10 or newer](https://www.mono-project.com/download/stable/). 21 | 22 | ## Developing 23 | 24 | The recommended way to develop on this project on all platforms is using [Visual Studio Code](https://code.visualstudio.com/) with the following extensions: 25 | 26 | * **Ionide-fsharp** for F# language support. 27 | * **C#** (optionally) for the server side debugger. 28 | 29 | To get running, start the "Run Build Task" command (`Ctrl+Shift+B` by default) and select the "watch" task. This starts a job that compiles the application, starts it at urls `http://localhost:5000` and `https://localhost:5001`, and restarts this process when you save an F# file. 30 | 31 | For the same effect from the command line, run `dotnet watch run` in the `UrlShortener` folder. Note that this needs the following environment variables set in the shell where you run it: 32 | * `ASPNETCORE_ENVIRONMENT`: `development` declares that this is a development environment (as opposed to eg. `staging` or `production`). It is used, among other things, to determine which `appSettings` file to use (see below). 33 | * If you're running Linux: `LD_LIBRARY_PATH`: `/packages/System.Data.SQLite.Core/runtimes/linux-x64/lib/netstandard2.0` allows the compiler and the application to find the required SQLite native library. 34 | * If you're running OSX: `LD_LIBRARY_PATH`: `/packages/System.Data.SQLite.Core/runtimes/osx-x64/lib/netstandard2.0` allows the compiler and the application to find the required SQLite native library. 35 | 36 | ![Screenshot: Visual Studio Code](./docs/vscode.png) 37 | 38 | ## Setting up the Facebook login provider 39 | 40 | The application demonstrates the use of OAuth login with Facebook. This means that you need a Facebook application set up to be able to log into the application. For this, you need to create a developer account on [the Facebook developer console](https://developers.facebook.com), then create an App and add Facebook Login to it. 41 | 42 | UrlShortener retrieves the application credentials (app ID and app Secret, which you can retrieve on the Facebook console under Settings > Basic) from the application configuration. The simplest way to set it up is to create a file called `appSettings.development.json` in the `UrlShortener` directory with the following content: 43 | 44 | ```json 45 | { 46 | "facebook": { 47 | "clientId": "your app id here", 48 | "clientSecret": "your app secret here" 49 | } 50 | } 51 | ``` 52 | 53 | It is *not* recommended to add the above to the general `appSettings.json`: this file is committed to git, and you should never commit such credentials to source control under any circumstances. 54 | 55 | ## Code walkthrough 56 | 57 | The application's source code is structured as follows. 58 | 59 | ![Diagram: internal code dependencies](./docs/code-structure.png) 60 | 61 | ### Common client and server-side 62 | 63 | * `DataModel.fs` contains the core data model types and functionality. 64 | * The `EndPoint` type defines the set of HTTP endpoints served by the application, and `Router` can create URLs from `EndPoint` values and vice-versa; 65 | * The `Link` and `User` modules define the model for stored links and users, respectively. Each defines a set of immutable data types and related pure functions. 66 | 67 | ### Client-side 68 | 69 | * `Client.fs` contains the Model-View-Update client-side implementation of the "My links" page. [Learn more about MVU](https://dotnet-websharper.github.io/mvu) 70 | * The `Model` module defines the state of the client-side page in terms of immutable data types. It also defines a `Message` type which lists all the possible events that can happen on this page. 71 | * The `View` module defines how to display the page based on this state. It uses a `dispatch` function to send `Message`s when something needs to happen. 72 | * The `Update` module defines what must happen when a `Message` is `dispatch`ed. This means modifying the state and/or triggering impure actions such as calling the server. 73 | * Finally the `MyLinks` function puts all these together and returns a value that can be directly included in a page. 74 | 75 | ### Server-side 76 | 77 | * `Database.fs` contains the database interaction code. 78 | * `Sql` instantiates SQLProvider to connect to the SQLite database, which is stored in the filesystem as `db/urlshortener.db`. 79 | * `Context` encapsulates the database context. It is declared as an ASP.NET Core service (see `ConfigureServices` in `Startup.fs`) in order to access the application settings and the logger through dependency injection. [Learn more about ASP.NET Core dependency injection](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-2.1) 80 | 81 | It contains methods for the various CRUD operations needed by the application, such as `CreateLink` or `GetAllUserLinks`. 82 | 83 | It also contains `Migrate()`, which is called on application startup to apply any outstanding migrations to the database. The migration scripts are located under `db/migrations`. 84 | * Finally, an extension property `Db` is added to `Web.Context` to easily retrieve a database `Context` from either a remote function or a sitelet content. 85 | * `Authentication.fs` contains the definition for the Facebook OAuth provider. It uses [`WebSharper.OAuth`](https://github.com/dotnet-websharper/oauth). 86 | * `Remoting.fs` contains server-side functions that are callable from the client side. They are basically wrappers for the corresponding database functions that additionally check for user authentication. 87 | * `Site.fs` contains the server side code for rendering the application's various endpoints. 88 | * Most pages use HTML templating to define their content. `MainTemplate` parses `Main.html` and extracts HTML snippets, marked with the `ws-template` attribute; F# code can then put these snippets together as needed. This also happens at run time, so editing `Main.html` does not require a recompile. [Learn more about WebSharper.UI templating](https://developers.websharper.com/docs/v4.x/fs/ui#templating) 89 | * `Site`, similarly to `Database.Context`, is declared as an ASP.NET Core service to gain access to dependency injection. 90 | * Its property `Sitelet` defines the site to serve. It unites together the handler from `FacebookProvider` with an `Application.MultiPage` that returns content based on the endpoint. 91 | * `Startup.fs` creates the ASP.NET Core application proper, registers WebSharper with it and runs database migrations on startup. 92 | 93 | ## Deploying to Azure 94 | 95 | This application is easy to deploy as an Azure web app. Here are the steps: 96 | 97 | * Create a Windows Web App on the [Azure Portal](https://portal.azure.com). 98 | * Under "Application settings", add the following application settings: 99 | * `SCM_SCRIPT_GENERATOR_ARGS`: `--aspNetCore UrlShortener\UrlShortener.fsproj` tells Azure where to find the application to run. 100 | * `facebook__clientId`: `your app id here` (note the double underscore) sets the OAuth client id for the Facebook application. It is equivalent to the above `appSettings.development.json`. 101 | * `facebook__clientSecret`: `your app id here` similarly sets the OAuth client secret. 102 | * Under "Deployment options", set up the deployment method of your choice. 103 | * "Local git" is a good option: it creates a git repository that you can simply push to and the application will be built and deployed. You will need to set up the git login/password under "Deployment credentials", and retrieve the git repository URL under "Deployment Center". 104 | * Alternately, if you forked this repository on Github, the "Github" option will trigger a deployment every time you push there, without having to push to a separate repository. 105 | 106 | [![Screenshot: Azure Portal](./docs/azure-portal.png)](https://portal.azure.com) 107 | 108 | ## Going further 109 | 110 | * [WebSharper home page](https://websharper.com) 111 | * [WebSharper documentation](https://developers.websharper.com) 112 | * [More WebSharper samples](https://github.com/websharper-samples) 113 | 114 | Happy coding! 115 | -------------------------------------------------------------------------------- /paket.lock: -------------------------------------------------------------------------------- 1 | STORAGE: NONE 2 | RESTRICTION: == netcoreapp3.1 3 | NUGET 4 | remote: https://api.nuget.org/v3/index.json 5 | Evolve (1.9) 6 | Microsoft.Build.Framework (>= 15.1.548) 7 | Microsoft.Build.Utilities.Core (>= 15.1.548) 8 | Microsoft.Extensions.DependencyModel (>= 2.0.3) 9 | System.Data.Common (>= 4.1) 10 | System.Runtime.Loader (>= 4.0) 11 | FSharp.Core (5.0.1) 12 | HtmlAgilityPack (1.11.33) 13 | Microsoft.AspNetCore.Authentication.Abstractions (2.2) 14 | Microsoft.AspNetCore.Http.Abstractions (>= 2.2) 15 | Microsoft.Extensions.Logging.Abstractions (>= 2.2) 16 | Microsoft.Extensions.Options (>= 2.2) 17 | Microsoft.AspNetCore.Hosting.Abstractions (2.2) 18 | Microsoft.AspNetCore.Hosting.Server.Abstractions (>= 2.2) 19 | Microsoft.AspNetCore.Http.Abstractions (>= 2.2) 20 | Microsoft.Extensions.Hosting.Abstractions (>= 2.2) 21 | Microsoft.AspNetCore.Hosting.Server.Abstractions (2.2) 22 | Microsoft.AspNetCore.Http.Features (>= 2.2) 23 | Microsoft.Extensions.Configuration.Abstractions (>= 2.2) 24 | Microsoft.AspNetCore.Http.Abstractions (2.2) 25 | Microsoft.AspNetCore.Http.Features (>= 2.2) 26 | System.Text.Encodings.Web (>= 4.5) 27 | Microsoft.AspNetCore.Http.Features (5.0.5) 28 | Microsoft.Extensions.Primitives (>= 5.0.1) 29 | System.IO.Pipelines (>= 5.0.1) 30 | Microsoft.Build.Framework (16.9) 31 | System.Security.Permissions (>= 4.7) 32 | Microsoft.Build.Utilities.Core (16.9) 33 | Microsoft.Build.Framework (>= 16.9) 34 | Microsoft.Win32.Registry (>= 4.3) 35 | System.Collections.Immutable (>= 5.0) 36 | System.Security.Permissions (>= 4.7) 37 | System.Text.Encoding.CodePages (>= 4.0.1) 38 | Microsoft.Extensions.Configuration.Abstractions (5.0) 39 | Microsoft.Extensions.Primitives (>= 5.0) 40 | Microsoft.Extensions.DependencyInjection.Abstractions (5.0) 41 | Microsoft.Extensions.DependencyModel (5.0) 42 | System.Text.Encodings.Web (>= 5.0) 43 | System.Text.Json (>= 5.0) 44 | Microsoft.Extensions.FileProviders.Abstractions (5.0) 45 | Microsoft.Extensions.Primitives (>= 5.0) 46 | Microsoft.Extensions.Hosting.Abstractions (5.0) 47 | Microsoft.Extensions.Configuration.Abstractions (>= 5.0) 48 | Microsoft.Extensions.DependencyInjection.Abstractions (>= 5.0) 49 | Microsoft.Extensions.FileProviders.Abstractions (>= 5.0) 50 | Microsoft.Extensions.Logging.Abstractions (5.0) 51 | Microsoft.Extensions.Options (5.0) 52 | Microsoft.Extensions.DependencyInjection.Abstractions (>= 5.0) 53 | Microsoft.Extensions.Primitives (>= 5.0) 54 | Microsoft.Extensions.Primitives (5.0.1) 55 | System.Runtime.CompilerServices.Unsafe (>= 5.0) 56 | Microsoft.NETCore.Platforms (5.0.2) 57 | Microsoft.NETCore.Targets (5.0) 58 | Microsoft.Win32.Registry (5.0) 59 | System.Security.AccessControl (>= 5.0) 60 | System.Security.Principal.Windows (>= 5.0) 61 | Microsoft.Win32.SystemEvents (5.0) 62 | Microsoft.NETCore.Platforms (>= 5.0) 63 | NETStandard.Library (2.0.3) 64 | Microsoft.NETCore.Platforms (>= 1.1) 65 | runtime.native.System.Data.SqlClient.sni (4.7) 66 | runtime.win-arm64.runtime.native.System.Data.SqlClient.sni (>= 4.4) 67 | runtime.win-x64.runtime.native.System.Data.SqlClient.sni (>= 4.4) 68 | runtime.win-x86.runtime.native.System.Data.SqlClient.sni (>= 4.4) 69 | runtime.win-arm64.runtime.native.System.Data.SqlClient.sni (4.4) 70 | runtime.win-x64.runtime.native.System.Data.SqlClient.sni (4.4) 71 | runtime.win-x86.runtime.native.System.Data.SqlClient.sni (4.4) 72 | SQLProvider (1.1.76) 73 | FSharp.Core (>= 3.1.2.5) 74 | NETStandard.Library (>= 2.0) 75 | System.Console (>= 4.3) 76 | System.Data.Common (>= 4.3) 77 | System.Data.Odbc (>= 4.5) 78 | System.Data.SqlClient (>= 4.4) 79 | System.IO (>= 4.3) 80 | System.Reflection (>= 4.3) 81 | System.Reflection.TypeExtensions (>= 4.3) 82 | System.Runtime (>= 4.3) 83 | System.Runtime.Loader (>= 4.3) 84 | System.Runtime.Serialization.Formatters (>= 4.3) 85 | System.Runtime.Serialization.Primitives (>= 4.3) 86 | System.Collections (4.3) 87 | Microsoft.NETCore.Platforms (>= 1.1) 88 | Microsoft.NETCore.Targets (>= 1.1) 89 | System.Runtime (>= 4.3) 90 | System.Collections.Immutable (5.0) 91 | System.Console (4.3.1) 92 | Microsoft.NETCore.Platforms (>= 1.1) 93 | Microsoft.NETCore.Targets (>= 1.1.2) 94 | System.IO (>= 4.3) 95 | System.Runtime (>= 4.3) 96 | System.Text.Encoding (>= 4.3) 97 | System.Data.Common (4.3) 98 | System.Collections (>= 4.3) 99 | System.Globalization (>= 4.3) 100 | System.IO (>= 4.3) 101 | System.Resources.ResourceManager (>= 4.3) 102 | System.Runtime (>= 4.3) 103 | System.Runtime.Extensions (>= 4.3) 104 | System.Text.RegularExpressions (>= 4.3) 105 | System.Threading.Tasks (>= 4.3) 106 | System.Data.Odbc (5.0) 107 | Microsoft.NETCore.Platforms (>= 5.0) 108 | System.Data.SqlClient (4.8.2) 109 | Microsoft.Win32.Registry (>= 4.7) 110 | runtime.native.System.Data.SqlClient.sni (>= 4.7) 111 | System.Security.Principal.Windows (>= 4.7) 112 | System.Data.SQLite.Core (1.0.112) - storage: packages 113 | System.Drawing.Common (5.0.2) 114 | Microsoft.Win32.SystemEvents (>= 5.0) 115 | System.Globalization (4.3) 116 | Microsoft.NETCore.Platforms (>= 1.1) 117 | Microsoft.NETCore.Targets (>= 1.1) 118 | System.Runtime (>= 4.3) 119 | System.IO (4.3) 120 | Microsoft.NETCore.Platforms (>= 1.1) 121 | Microsoft.NETCore.Targets (>= 1.1) 122 | System.Runtime (>= 4.3) 123 | System.Text.Encoding (>= 4.3) 124 | System.Threading.Tasks (>= 4.3) 125 | System.IO.Pipelines (5.0.1) 126 | System.Reflection (4.3) 127 | Microsoft.NETCore.Platforms (>= 1.1) 128 | Microsoft.NETCore.Targets (>= 1.1) 129 | System.IO (>= 4.3) 130 | System.Reflection.Primitives (>= 4.3) 131 | System.Runtime (>= 4.3) 132 | System.Reflection.Emit.Lightweight (4.7) 133 | System.Reflection.Primitives (4.3) 134 | Microsoft.NETCore.Platforms (>= 1.1) 135 | Microsoft.NETCore.Targets (>= 1.1) 136 | System.Runtime (>= 4.3) 137 | System.Reflection.TypeExtensions (4.7) 138 | System.Resources.ResourceManager (4.3) 139 | Microsoft.NETCore.Platforms (>= 1.1) 140 | Microsoft.NETCore.Targets (>= 1.1) 141 | System.Globalization (>= 4.3) 142 | System.Reflection (>= 4.3) 143 | System.Runtime (>= 4.3) 144 | System.Runtime (4.3.1) 145 | Microsoft.NETCore.Platforms (>= 1.1.1) 146 | Microsoft.NETCore.Targets (>= 1.1.3) 147 | System.Runtime.CompilerServices.Unsafe (5.0) 148 | System.Runtime.Extensions (4.3.1) 149 | Microsoft.NETCore.Platforms (>= 1.1.1) 150 | Microsoft.NETCore.Targets (>= 1.1.3) 151 | System.Runtime (>= 4.3.1) 152 | System.Runtime.Loader (4.3) 153 | System.IO (>= 4.3) 154 | System.Reflection (>= 4.3) 155 | System.Runtime (>= 4.3) 156 | System.Runtime.Serialization.Formatters (4.3) 157 | System.Collections (>= 4.3) 158 | System.Reflection (>= 4.3) 159 | System.Resources.ResourceManager (>= 4.3) 160 | System.Runtime (>= 4.3) 161 | System.Runtime.Serialization.Primitives (>= 4.3) 162 | System.Runtime.Serialization.Primitives (4.3) 163 | System.Resources.ResourceManager (>= 4.3) 164 | System.Runtime (>= 4.3) 165 | System.Security.AccessControl (5.0) 166 | Microsoft.NETCore.Platforms (>= 5.0) 167 | System.Security.Principal.Windows (>= 5.0) 168 | System.Security.Permissions (5.0) 169 | System.Security.AccessControl (>= 5.0) 170 | System.Windows.Extensions (>= 5.0) 171 | System.Security.Principal.Windows (5.0) 172 | System.Text.Encoding (4.3) 173 | Microsoft.NETCore.Platforms (>= 1.1) 174 | Microsoft.NETCore.Targets (>= 1.1) 175 | System.Runtime (>= 4.3) 176 | System.Text.Encoding.CodePages (5.0) 177 | Microsoft.NETCore.Platforms (>= 5.0) 178 | System.Runtime.CompilerServices.Unsafe (>= 5.0) 179 | System.Text.Encodings.Web (5.0.1) 180 | System.Text.Json (5.0.2) 181 | System.Runtime.CompilerServices.Unsafe (>= 5.0) 182 | System.Text.Encodings.Web (>= 5.0.1) 183 | System.Text.RegularExpressions (4.3.1) 184 | System.Runtime (>= 4.3.1) 185 | System.Threading.Tasks (4.3) 186 | Microsoft.NETCore.Platforms (>= 1.1) 187 | Microsoft.NETCore.Targets (>= 1.1) 188 | System.Runtime (>= 4.3) 189 | System.Windows.Extensions (5.0) 190 | System.Drawing.Common (>= 5.0) 191 | WebSharper (4.7.1.432) 192 | FSharp.Core (>= 4.2.3) 193 | System.Reflection.Emit.Lightweight (>= 4.3) 194 | WebSharper.AspNetCore (4.7.1.159) 195 | FSharp.Core (>= 4.3.4) 196 | Microsoft.AspNetCore.Authentication.Abstractions (>= 2.0) 197 | Microsoft.AspNetCore.Hosting.Abstractions (>= 2.0) 198 | Microsoft.AspNetCore.Http.Abstractions (>= 2.0) 199 | Microsoft.Extensions.Configuration.Abstractions (>= 2.0) 200 | WebSharper (>= 4.7.1 < 4.8) 201 | WebSharper.FSharp (4.7.1.432) 202 | WebSharper (4.7.1.432) 203 | WebSharper.Mvu (4.7.0.101) 204 | WebSharper (>= 4.7 < 4.8) 205 | WebSharper.UI (>= 4.7 < 4.8) 206 | WebSharper.OAuth (4.7.1.200) 207 | WebSharper (>= 4.7.1 < 4.8) 208 | WebSharper.UI (4.7.1.234) 209 | HtmlAgilityPack (>= 1.7.1) 210 | WebSharper (>= 4.7.1 < 4.8) 211 | -------------------------------------------------------------------------------- /UrlShortener/Client.fs: -------------------------------------------------------------------------------- 1 | [] 2 | module UrlShortener.Client 3 | 4 | open WebSharper 5 | open WebSharper.JavaScript 6 | open WebSharper.UI 7 | open WebSharper.UI.Html 8 | open WebSharper.UI.Client 9 | open WebSharper.Mvu 10 | open UrlShortener.DataModel 11 | 12 | /// Defines the model of this client side page, 13 | /// ie. the data types that represent the state and its transitions. 14 | module Model = 15 | 16 | /// The state of a link. 17 | type LinkState = 18 | { 19 | LinkUrl: string 20 | TargetUrl: string 21 | VisitCount: int64 22 | Deleting: bool 23 | } 24 | 25 | /// The state of the full page. 26 | type State = 27 | { 28 | User: User.Data 29 | Links: Map 30 | IsLoading: bool 31 | NewLinkText: string 32 | PromptingDelete: Link.Slug option 33 | } 34 | 35 | member this.TotalVisitCount = 36 | Map.fold (fun count _ link -> count + link.VisitCount) 37 | 0L this.Links 38 | 39 | /// The initial state of the page. 40 | let InitialState user = 41 | { 42 | User = user 43 | Links = Map.empty 44 | IsLoading = true 45 | NewLinkText = "" 46 | PromptingDelete = None 47 | } 48 | 49 | /// The full list of actions that can be performed on the state. 50 | type Message = 51 | | Refresh 52 | | Refreshed of links: Link.Data[] 53 | | PromptDelete of slug: Link.Slug option 54 | | ConfirmDelete of slug: Link.Slug 55 | | SetNewLinkText of string 56 | | CreateNewLink 57 | 58 | /// Defines the view of the page, ie how the state is rendered to HTML. 59 | module View = 60 | open Model 61 | 62 | /// The view for the "Shorten this link" form. 63 | let NewLinkForm dispatch (state: View) = 64 | form [ 65 | on.submit (fun _ ev -> 66 | ev.PreventDefault() 67 | dispatch CreateNewLink) 68 | attr.``class`` "field has-addons has-addons-centered" 69 | ] [ 70 | div [attr.``class`` "control"] [ 71 | input [ 72 | attr.``class`` "input is-large" 73 | attr.placeholder "Shorten this link" 74 | attr.value state.V.NewLinkText 75 | on.change (fun el _ -> dispatch (SetNewLinkText el?value)) 76 | ] [] 77 | ] 78 | div [attr.``class`` "control"] [ 79 | button [ 80 | attr.``class`` "button is-info is-large" 81 | Attr.ClassPred "is-loading" state.V.IsLoading 82 | ] [ 83 | text "Shorten" 84 | ] 85 | ] 86 | ] 87 | 88 | /// The view for the "Update list" button. 89 | let UpdateButton dispatch (state: View) = 90 | button [ 91 | on.click (fun _ _ -> dispatch Refresh) 92 | attr.``class`` "button is-info" 93 | Attr.ClassPred "is-loading" state.V.IsLoading 94 | ] [ 95 | text "Update list" 96 | ] 97 | 98 | /// The view for the "Delete" button for a given link. 99 | let DeleteButton dispatch slug (link: View) = 100 | button [ 101 | on.click (fun _ _ -> dispatch (PromptDelete (Some slug))) 102 | attr.``class`` "button is-info" 103 | Attr.ClassPred "is-loading" link.V.Deleting 104 | ] [ 105 | text "Delete" 106 | ] 107 | 108 | /// A link whose text is the URL itself. 109 | let UrlLink (link: View) = 110 | a [attr.href link.V; attr.target "_blank"] [text link.V] 111 | 112 | /// The row for a link in the table. 113 | let LinkRow dispatch slug (link: View) = 114 | tr [] [ 115 | td [] [UrlLink (V link.V.LinkUrl)] 116 | td [] [UrlLink (V link.V.TargetUrl)] 117 | td [] [text (string link.V.VisitCount)] 118 | td [] [DeleteButton dispatch slug link] 119 | ] 120 | 121 | /// A row that indicates that there are no links, if that's the case. 122 | let NoLinks (state: View) = 123 | V( 124 | if Map.isEmpty state.V.Links then 125 | tr [] [ 126 | td [ 127 | attr.``class`` "has-text-centered" 128 | attr.colspan "4" 129 | ] [ 130 | text "You haven't created any links yet." 131 | ] 132 | ] 133 | else 134 | Doc.Empty) 135 | |> Doc.EmbedView 136 | 137 | /// The view for the table of links. 138 | let LinksTable dispatch (state: View) = 139 | table [attr.``class`` "table is-fullwidth"] [ 140 | thead [] [ 141 | tr [] [ 142 | th [] [text "Link"] 143 | th [] [text "Target"] 144 | th [] [text "Visits"] 145 | th [] [UpdateButton dispatch state] 146 | ] 147 | ] 148 | tbody [] [ 149 | (V state.V.Links).DocSeqCached(LinkRow dispatch) 150 | NoLinks state 151 | ] 152 | tfoot [] [ 153 | tr [] [ 154 | th [] [] 155 | th [attr.``class`` "has-text-right"] [text "Total:"] 156 | th [] [text (string state.V.TotalVisitCount)] 157 | th [] [] 158 | ] 159 | ] 160 | ] 161 | 162 | /// The modal that prompts for deleting a link. 163 | let ModalDelete dispatch (state: View) = 164 | div [ 165 | attr.``class`` "modal" 166 | Attr.ClassPred "is-active" state.V.PromptingDelete.IsSome 167 | ] [ 168 | div [attr.``class`` "modal-background"] [] 169 | div [attr.``class`` "modal-content box"] [ 170 | p [attr.``class`` "subtitle" ] [ 171 | text "Do you really want to delete this link?" 172 | ] 173 | div [attr.``class`` "buttons is-right"] [ 174 | a [ 175 | attr.``class`` "button is-danger" 176 | on.clickView state (fun _ _ v -> 177 | dispatch (ConfirmDelete v.PromptingDelete.Value)) 178 | ] [ 179 | text "Yes" 180 | ] 181 | a [ 182 | attr.``class`` "button" 183 | on.click (fun _ _ -> dispatch (PromptDelete None)) 184 | ] [ 185 | text "No" 186 | ] 187 | ] 188 | ] 189 | ] 190 | 191 | /// The view for the full page content. 192 | let Page dispatch (state: View) = 193 | Doc.Concat [ 194 | h1 [attr.``class`` "title has-text-centered"] [ 195 | text ("Welcome, " + state.V.User.FullName + "!") 196 | ] 197 | NewLinkForm dispatch state 198 | p [attr.``class`` "subtitle"] [ 199 | text "Here are the links you have created:" 200 | ] 201 | LinksTable dispatch state 202 | ModalDelete dispatch state 203 | ] 204 | 205 | /// Defines what must happen (eg. change the state, start async tasks) 206 | /// when a message is dispatched. 207 | module Update = 208 | open Model 209 | 210 | // /// This is an artificially slowed-down version of DispatchAsync 211 | // /// so that we can see the loading buttons even though 212 | // /// local remote calls run pretty much instantly. 213 | // let DispatchAsync msg call = 214 | // DispatchAsync msg (async { 215 | // let! res = call 216 | // do! Async.Sleep 1000 217 | // return res 218 | // }) 219 | 220 | let Update (message: Message) (state: State) = 221 | match message with 222 | | Refresh -> 223 | SetModel { state with IsLoading = true } 224 | + 225 | DispatchAsync Refreshed (Remoting.GetMyLinks ()) 226 | | PromptDelete slug -> 227 | SetModel { state with PromptingDelete = slug } 228 | | ConfirmDelete slug -> 229 | SetModel { state with PromptingDelete = None } 230 | + 231 | DispatchAsync Refreshed (Remoting.DeleteLink slug) 232 | | Refreshed links -> 233 | let links = 234 | links 235 | |> Array.map (fun link -> 236 | link.Slug, { 237 | LinkUrl = link.LinkUrl 238 | TargetUrl = link.TargetUrl 239 | VisitCount = link.VisitCount 240 | Deleting = false 241 | } 242 | ) 243 | |> Map.ofArray 244 | SetModel { state with Links = links; IsLoading = false } 245 | | SetNewLinkText url -> 246 | SetModel { state with NewLinkText = url } 247 | | CreateNewLink -> 248 | SetModel { state with IsLoading = true; NewLinkText = "" } 249 | + 250 | DispatchAsync Refreshed (Remoting.CreateNewLink state.NewLinkText) 251 | 252 | open Model 253 | open View 254 | open Update 255 | 256 | /// Binds together the model, the view and the update. 257 | let MyLinks (user: User.Data) = 258 | App.Create (InitialState user) Update Page 259 | |> App.WithInitAction (Command (fun dispatch -> dispatch Refresh)) 260 | |> App.Run 261 | -------------------------------------------------------------------------------- /.paket/Paket.Restore.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | $(MSBuildAllProjects);$(MSBuildThisFileFullPath) 8 | 9 | $(MSBuildVersion) 10 | 15.0.0 11 | false 12 | true 13 | 14 | true 15 | $(MSBuildThisFileDirectory) 16 | $(MSBuildThisFileDirectory)..\ 17 | $(PaketRootPath)paket-files\paket.restore.cached 18 | $(PaketRootPath)paket.lock 19 | classic 20 | proj 21 | assembly 22 | native 23 | /Library/Frameworks/Mono.framework/Commands/mono 24 | mono 25 | 26 | 27 | $(PaketRootPath)paket.bootstrapper.exe 28 | $(PaketToolsPath)paket.bootstrapper.exe 29 | $([System.IO.Path]::GetDirectoryName("$(PaketBootStrapperExePath)"))\ 30 | 31 | "$(PaketBootStrapperExePath)" 32 | $(MonoPath) --runtime=v4.0.30319 "$(PaketBootStrapperExePath)" 33 | 34 | 35 | 36 | 37 | true 38 | true 39 | 40 | 41 | True 42 | 43 | 44 | False 45 | 46 | $(BaseIntermediateOutputPath.TrimEnd('\').TrimEnd('\/')) 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | $(PaketRootPath)paket 56 | $(PaketToolsPath)paket 57 | 58 | 59 | 60 | 61 | 62 | $(PaketRootPath)paket.exe 63 | $(PaketToolsPath)paket.exe 64 | 65 | 66 | 67 | 68 | 69 | <_DotnetToolsJson Condition="Exists('$(PaketRootPath)/.config/dotnet-tools.json')">$([System.IO.File]::ReadAllText("$(PaketRootPath)/.config/dotnet-tools.json")) 70 | <_ConfigContainsPaket Condition=" '$(_DotnetToolsJson)' != ''">$(_DotnetToolsJson.Contains('"paket"')) 71 | <_ConfigContainsPaket Condition=" '$(_ConfigContainsPaket)' == ''">false 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | <_PaketCommand>dotnet paket 83 | 84 | 85 | 86 | 87 | 88 | $(PaketToolsPath)paket 89 | $(PaketBootStrapperExeDir)paket 90 | 91 | 92 | paket 93 | 94 | 95 | 96 | 97 | <_PaketExeExtension>$([System.IO.Path]::GetExtension("$(PaketExePath)")) 98 | <_PaketCommand Condition=" '$(_PaketCommand)' == '' AND '$(_PaketExeExtension)' == '.dll' ">dotnet "$(PaketExePath)" 99 | <_PaketCommand Condition=" '$(_PaketCommand)' == '' AND '$(OS)' != 'Windows_NT' AND '$(_PaketExeExtension)' == '.exe' ">$(MonoPath) --runtime=v4.0.30319 "$(PaketExePath)" 100 | <_PaketCommand Condition=" '$(_PaketCommand)' == '' ">"$(PaketExePath)" 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | true 122 | $(NoWarn);NU1603;NU1604;NU1605;NU1608 123 | false 124 | true 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | $([System.IO.File]::ReadAllText('$(PaketRestoreCacheFile)')) 134 | 135 | 136 | 137 | 138 | 139 | 141 | $([System.Text.RegularExpressions.Regex]::Split(`%(Identity)`, `": "`)[0].Replace(`"`, ``).Replace(` `, ``)) 142 | $([System.Text.RegularExpressions.Regex]::Split(`%(Identity)`, `": "`)[1].Replace(`"`, ``).Replace(` `, ``)) 143 | 144 | 145 | 146 | 147 | %(PaketRestoreCachedKeyValue.Value) 148 | %(PaketRestoreCachedKeyValue.Value) 149 | 150 | 151 | 152 | 153 | true 154 | false 155 | true 156 | 157 | 158 | 162 | 163 | true 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | $(PaketIntermediateOutputPath)\$(MSBuildProjectFile).paket.references.cached 183 | 184 | $(MSBuildProjectFullPath).paket.references 185 | 186 | $(MSBuildProjectDirectory)\$(MSBuildProjectName).paket.references 187 | 188 | $(MSBuildProjectDirectory)\paket.references 189 | 190 | false 191 | true 192 | true 193 | references-file-or-cache-not-found 194 | 195 | 196 | 197 | 198 | $([System.IO.File]::ReadAllText('$(PaketReferencesCachedFilePath)')) 199 | $([System.IO.File]::ReadAllText('$(PaketOriginalReferencesFilePath)')) 200 | references-file 201 | false 202 | 203 | 204 | 205 | 206 | false 207 | 208 | 209 | 210 | 211 | true 212 | target-framework '$(TargetFramework)' or '$(TargetFrameworks)' files @(PaketResolvedFilePaths) 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | false 224 | true 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',').Length) 236 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[0]) 237 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[1]) 238 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[4]) 239 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[5]) 240 | 241 | 242 | %(PaketReferencesFileLinesInfo.PackageVersion) 243 | All 244 | runtime 245 | runtime 246 | true 247 | true 248 | 249 | 250 | 251 | 252 | $(PaketIntermediateOutputPath)/$(MSBuildProjectFile).paket.clitools 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | $([System.String]::Copy('%(PaketCliToolFileLines.Identity)').Split(',')[0]) 262 | $([System.String]::Copy('%(PaketCliToolFileLines.Identity)').Split(',')[1]) 263 | 264 | 265 | %(PaketCliToolFileLinesInfo.PackageVersion) 266 | 267 | 268 | 269 | 273 | 274 | 275 | 276 | 277 | 278 | false 279 | 280 | 281 | 282 | 283 | 284 | <_NuspecFilesNewLocation Include="$(PaketIntermediateOutputPath)\$(Configuration)\*.nuspec"/> 285 | 286 | 287 | 288 | 289 | 290 | $(MSBuildProjectDirectory)/$(MSBuildProjectFile) 291 | true 292 | false 293 | true 294 | false 295 | true 296 | false 297 | true 298 | false 299 | true 300 | $(PaketIntermediateOutputPath)\$(Configuration) 301 | $(PaketIntermediateOutputPath) 302 | 303 | 304 | 305 | <_NuspecFiles Include="$(AdjustedNuspecOutputPath)\*.$(PackageVersion.Split(`+`)[0]).nuspec"/> 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 363 | 364 | 407 | 408 | 450 | 451 | 492 | 493 | 494 | 495 | --------------------------------------------------------------------------------