From c1daa88869bbc10fd6b7fa1ed8ad91f105fcd282 Mon Sep 17 00:00:00 2001 From: Dustin Moris Gorski Date: Sat, 20 Mar 2021 16:33:22 +0000 Subject: [PATCH 1/7] MailDrop change --- .gitignore | 5 +- src/DustedCodes/Captcha.fs | 28 ++--- src/DustedCodes/Config.fs | 14 +-- src/DustedCodes/DustedCodes.fsproj | 3 - src/DustedCodes/Extensions.fs | 12 --- src/DustedCodes/Helpers.fs | 40 +++++-- src/DustedCodes/HttpHandlers.fs | 9 +- src/DustedCodes/Messages.fs | 167 ++++------------------------- src/DustedCodes/Program.fs | 137 ++++++++++++----------- 9 files changed, 146 insertions(+), 269 deletions(-) diff --git a/.gitignore b/.gitignore index b501d1d..b08e154 100644 --- a/.gitignore +++ b/.gitignore @@ -337,4 +337,7 @@ ASALocalRun/ # DotEnv .env -.env.ini \ No newline at end of file +.env.ini + +# VS Code +.devcontainer \ No newline at end of file diff --git a/src/DustedCodes/Captcha.fs b/src/DustedCodes/Captcha.fs index 06d6775..a64e82e 100644 --- a/src/DustedCodes/Captcha.fs +++ b/src/DustedCodes/Captcha.fs @@ -4,8 +4,7 @@ namespace DustedCodes module Captcha = open System open System.Net - open System.Net.Http - open System.Diagnostics + open System.Threading.Tasks open FSharp.Control.Tasks.NonAffine open Newtonsoft.Json @@ -41,29 +40,24 @@ module Captcha = UserError "Verification failed. Please try again." | _ -> ServerError (sprintf "Unknown error code: %s" errorCode) + type ValidateFunc = string -> string -> string -> Task + let validate - (log : Log.Func) - (client : HttpClient) + (postCaptcha : Http.PostFormFunc) (siteKey : string) (secretKey : string) (captchaResponse : string) = task { - let url = "https://hcaptcha.com/siteverify" let data = dict [ "siteKey", siteKey "secret", secretKey "response", captchaResponse ] - - let timer = Stopwatch.StartNew() - let! statusCode, body = Http.postAsync client url data - timer.Stop() - log Level.Debug (sprintf "Validated captcha in %s" (timer.Elapsed.ToMs())) - + let! result = postCaptcha data return - if not (statusCode.Equals HttpStatusCode.OK) - then ServerError body - else - let result = JsonConvert.DeserializeObject body - match result.IsValid with + match result with + | Error err -> ServerError err + | Ok json -> + let captchaResult = JsonConvert.DeserializeObject json + match captchaResult.IsValid with | true -> Success - | false -> parseError (result.ErrorCodes.[0]) + | false -> parseError (captchaResult.ErrorCodes.[0]) } diff --git a/src/DustedCodes/Config.fs b/src/DustedCodes/Config.fs index a08792d..9dd10d8 100644 --- a/src/DustedCodes/Config.fs +++ b/src/DustedCodes/Config.fs @@ -129,19 +129,19 @@ module Config = [] type Mail = { - GcpDatastoreKind : string - GcpPubSubTopic : string + MailDropEndpoint : string + MailDropApiKey : string Domain : string Sender : string Recipient : string } static member Load() = { - GcpDatastoreKind = Env.varOrDefault "GCP_DS_CONTACT_MESSAGE_KIND" "" - GcpPubSubTopic = Env.varOrDefault "GCP_PS_EMAILS_TOPIC" "" + MailDropEndpoint = Env.varOrDefault "MAIL_DROP_ENDPOINT" "" + MailDropApiKey = Env.varOrDefault "MAIL_DROP_API_KEY" "" Domain = Env.varOrDefault "MAIL_DOMAIN" "" Sender = Env.varOrDefault "MAIL_SENDER" "" - Recipient = Env.varOrDefault "CONTACT_MESSAGES_RECIPIENT" "" + Recipient = Env.varOrDefault "MAIL_RECIPIENT" "" } [] @@ -220,8 +220,8 @@ module Config = "Project ID", this.GCP.ProjectId ] "Mail", dict [ - "GCP Datastore Kind", this.Mail.GcpDatastoreKind - "GCP PubSub Topic", this.Mail.GcpPubSubTopic + "MailDrop Endpoint", this.Mail.MailDropEndpoint + "MailDrop API Key", (this.Mail.MailDropApiKey.ToSecret()) "Mail Domain", this.Mail.Domain "Mail Sender", this.Mail.Sender "Mail Recipient", this.Mail.Recipient diff --git a/src/DustedCodes/DustedCodes.fsproj b/src/DustedCodes/DustedCodes.fsproj index e377c6b..fa89063 100644 --- a/src/DustedCodes/DustedCodes.fsproj +++ b/src/DustedCodes/DustedCodes.fsproj @@ -16,9 +16,6 @@ - - - diff --git a/src/DustedCodes/Extensions.fs b/src/DustedCodes/Extensions.fs index adc69da..a5a189e 100644 --- a/src/DustedCodes/Extensions.fs +++ b/src/DustedCodes/Extensions.fs @@ -94,18 +94,6 @@ module Extensions = | Some f -> f :?> Log.Func | None -> Log.write Log.consoleFormat [] (Level.Debug) None "" "" - member this.GetHttpClient - (userAgent : string) - (trace : (string * string) option) = - let factory = this.GetService() - let client = factory.CreateClient(Http.clientName) - client.DefaultRequestHeaders.Add("User-Agent", userAgent) - match trace with - | None -> () - | Some (key, value) -> - client.DefaultRequestHeaders.Add(key, value) - client - type IServiceCollection with member this.When(predicate, svcFunc) = match predicate with diff --git a/src/DustedCodes/Helpers.fs b/src/DustedCodes/Helpers.fs index fa6a006..17bf3d1 100644 --- a/src/DustedCodes/Helpers.fs +++ b/src/DustedCodes/Helpers.fs @@ -53,18 +53,44 @@ module Network = [] module Http = - open System.Collections.Generic + open System.Text open System.Net.Http + open System.Threading.Tasks + open System.Collections.Generic open FSharp.Control.Tasks.NonAffine + open Newtonsoft.Json + + type PostResult = Task> + type PostFormFunc = IDictionary -> PostResult + type PostJsonFunc = obj -> PostResult + + let private postReq (client : HttpClient) (req : HttpRequestMessage) : PostResult = + task { + try + let! resp = client.SendAsync req + let! body = resp.Content.ReadAsStringAsync() + return + match resp.IsSuccessStatusCode with + | true -> Ok body + | false -> Error body + with ex -> + JsonConvert.SerializeObject(ex, Formatting.Indented) |> System.Console.WriteLine + return Error ex.Message + } - let clientName = "DefaultOutgoing" + let postForm (client : HttpClient) (form : IDictionary) = + task { + use data = new FormUrlEncodedContent(form) + use req = new HttpRequestMessage(Method = HttpMethod.Post, Content = data) + return! postReq client req + } - let postAsync (client : HttpClient) (url : string) (data : IDictionary) = + let postJson (client : HttpClient) (data : obj) = task { - let content = new FormUrlEncodedContent(data) - let! result = client.PostAsync(url, content) - let! response = result.Content.ReadAsStringAsync() - return (result.StatusCode, response) + let json = JsonConvert.SerializeObject data + use data = new StringContent(json, Encoding.UTF8, "application/json") + use req = new HttpRequestMessage(Method = HttpMethod.Post, Content = data) + return! postReq client req } // --------------------------------- diff --git a/src/DustedCodes/HttpHandlers.fs b/src/DustedCodes/HttpHandlers.fs index 89fbb0f..f65c800 100644 --- a/src/DustedCodes/HttpHandlers.fs +++ b/src/DustedCodes/HttpHandlers.fs @@ -117,15 +117,10 @@ module HttpHandlers = match msg.IsValid with | Error err -> return! respond msg (Error err) | Ok _ -> - let httpClient = - ctx.GetHttpClient - "dusted.codes/captcha-validator" - None + let validateCaptcha = ctx.GetService() let! captchaResult = ctx.Request.Form.["h-captcha-response"].ToString() - |> Captcha.validate - log - httpClient + |> validateCaptcha settings.ThirdParties.CaptchaSiteKey settings.ThirdParties.CaptchaSecretKey match captchaResult with diff --git a/src/DustedCodes/Messages.fs b/src/DustedCodes/Messages.fs index e51a3d2..ef5fecb 100644 --- a/src/DustedCodes/Messages.fs +++ b/src/DustedCodes/Messages.fs @@ -1,63 +1,14 @@ namespace DustedCodes [] -module Datastore = +module Messages = open System - open System.Threading.Tasks - open Google.Cloud.Datastore.V1 - open FSharp.Control.Tasks.NonAffine - - type SaveEntityFunc = Log.Func -> Entity -> Task> - - let toStringValue (str : string) = - Value(StringValue = str) - - let toTextValue (str : string) = - let v = Value(StringValue = str) - v.ExcludeFromIndexes <- true - v - - let toTimestampValue (dt : DateTime) = - let ts = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime dt - Value(TimestampValue = ts) - - let saveEntity - (appName : string) - (envName : string) - (entityKind : string) - (db : DatastoreDb) = - let keyFactory = db.CreateKeyFactory entityKind - fun (log : Log.Func) (entity : Entity) -> - task { - try - let key = keyFactory.CreateIncompleteKey() - entity.Key <- key - entity.["Origin"] <- toStringValue appName - entity.["Environment"] <- toStringValue envName - - let! keys = db.InsertAsync [ entity ] - let insertedKey = keys.[0].ToString() - log Level.Notice - (sprintf "A new entity (%s) has been successfully saved." insertedKey) - return Ok insertedKey - with ex -> - log Level.Alert - (sprintf "Failed to save entity in datastore: %s\n\n%s" - ex.Message - ex.StackTrace) - return Error ex.Message - } - -module PubSub = - open System.Threading.Tasks open System.Collections.Generic - open Google.Protobuf - open Google.Cloud.PubSub.V1 + open System.Threading.Tasks open FSharp.Control.Tasks.NonAffine - open Newtonsoft.Json [] - type Message = + type Request = { Domain : string Sender : string @@ -69,57 +20,6 @@ module PubSub = TemplateData : IDictionary } - type SendMessageFunc = Log.Func -> Message -> Task> - - let sendMessage - (envName : string) - (domain : string) - (sender : string) - (recipient : string) - (client : PublisherClient) = - fun (log : Log.Func) (msg : Message) -> - task { - try - let data = msg.TemplateData - let msg = - { msg with - Domain = domain - Sender = sender - Recipients = [ recipient ] - TemplateData = Dictionary() - } - - data.Keys - |> Seq.iter(fun key -> msg.TemplateData.Add(key, data.[key])) - msg.TemplateData.Add("environmentName", envName) - - let data = - msg - |> JsonConvert.SerializeObject - |> ByteString.CopyFromUtf8 - let pubSubMsg = PubsubMessage(Data = data) - pubSubMsg.Attributes.Add("encoding", "json-utf8") - - let! messageId = client.PublishAsync(pubSubMsg) - log Level.Notice - (sprintf "A new message (%s) has been successfully sent." messageId) - - return Ok messageId - with ex -> - log Level.Alert - (sprintf "Failed to publish pubsub message: %s\n\n%s" - ex.Message - ex.StackTrace) - return Error ex.Message - } - -[] -module Messages = - open System - open System.Threading.Tasks - open Google.Cloud.Datastore.V1 - open FSharp.Control.Tasks.NonAffine - [] type ContactMsg = { @@ -146,17 +46,22 @@ module Messages = else if String.IsNullOrEmpty this.Message then Error "Message cannot be empty." else Ok () - member this.ToPubSubMessage() = + member this.ToRequest + (envName : string) + (domain : string) + (sender : string) + (recipient : string) = { - Domain = "" - Sender = "" - Recipients = [] + Domain = domain + Sender = sender + Recipients = [ recipient ] CC = [] BCC = [] Subject = "Dusted Codes: A new message has been posted" TemplateName = "contact-message" TemplateData = dict [ + "environmentName", envName "msgSubject", this.Subject "msgContent", this.Message "msgDate", DateTimeOffset.Now.ToString("u") @@ -164,50 +69,24 @@ module Messages = "msgSenderEmail", this.Email "msgSenderPhone", this.Phone ] - } : PubSub.Message - - member this.ToDatastoreEntity() = - let entity = Entity() - entity.["Name"] <- this.Name |> Datastore.toStringValue - entity.["Email"] <- this.Email |> Datastore.toStringValue - entity.["Phone"] <- this.Phone |> Datastore.toStringValue - entity.["Subject"] <- this.Subject |> Datastore.toStringValue - entity.["Message"] <- this.Message |> Datastore.toTextValue - entity.["Date"] <- DateTime.UtcNow |> Datastore.toTimestampValue - entity + } : Request type SaveFunc = Log.Func -> ContactMsg -> Task> - let rec waitForFirstSuccess (tasks : Task> list) = - task { - let! task = Task.WhenAny tasks - match task.Result with - | Ok _ -> return task.Result - | Error _ -> - return! - tasks - |> List.filter(fun t -> t = task) - |> waitForFirstSuccess - } - let save - (saveEntity : Datastore.SaveEntityFunc) - (publishMsg : PubSub.SendMessageFunc) = + (postJson : Http.PostJsonFunc) + (envName : string) + (domain : string) + (sender : string) + (recipient : string) = fun (log : Log.Func) (msg : ContactMsg) -> task { - let dsEntity = msg.ToDatastoreEntity() - let dsTask = saveEntity log dsEntity - - let pubSubMsg = msg.ToPubSubMessage() - let pubSubTask = publishMsg log pubSubMsg - - let! result = - waitForFirstSuccess([ - dsTask - pubSubTask ]) - + let data = msg.ToRequest envName domain sender recipient + let! result = postJson data return match result with | Ok _ -> Ok "Thank you, your message has been successfully sent!" - | Error _ -> Error "Message could not be saved. Please try again later." + | Error err -> + log Level.Error (sprintf "Failed to send message to MailDrop: %s" err) + Error "Message could not be saved. Please try again later." } \ No newline at end of file diff --git a/src/DustedCodes/Program.fs b/src/DustedCodes/Program.fs index dad9874..068e2bd 100644 --- a/src/DustedCodes/Program.fs +++ b/src/DustedCodes/Program.fs @@ -2,6 +2,7 @@ namespace DustedCodes module Program = open System + open System.Net.Http open Microsoft.AspNetCore.Builder open Microsoft.AspNetCore.Hosting open Microsoft.Extensions.Hosting @@ -9,47 +10,46 @@ module Program = open Microsoft.Extensions.Caching.Distributed open Giraffe open Giraffe.EndpointRouting - open Google.Cloud.Datastore.V1 - open Google.Cloud.PubSub.V1 - open Google.Cloud.Diagnostics.AspNetCore - open Google.Cloud.Diagnostics.Common - - let mutable private pubSubClient : PublisherClient = null let configureServices (settings : Config.Settings) = fun (services : IServiceCollection) -> - let topicName = - TopicName( - settings.GCP.ProjectId, - settings.Mail.GcpPubSubTopic) - pubSubClient <- PublisherClient.CreateAsync(topicName).Result - let dsClient = DatastoreDb.Create settings.GCP.ProjectId + let captchaClient = new HttpClient() + captchaClient.BaseAddress <- + Uri("https://hcaptcha.com/siteverify") + captchaClient.DefaultRequestHeaders.Add( + "User-Agent", + "dusted.codes") + let postCaptcha = Http.postForm captchaClient + let validateCaptcha = Captcha.validate postCaptcha - let saveEntityFunc = - Datastore.saveEntity - settings.General.AppName - settings.General.EnvName - settings.Mail.GcpDatastoreKind - dsClient - let publishMsgFunc = - PubSub.sendMessage + let mailDropClient = new HttpClient() + mailDropClient.BaseAddress <- + Uri(settings.Mail.MailDropEndpoint) + mailDropClient.DefaultRequestHeaders.Add( + "Authorization", + sprintf "Bearer %s" settings.Mail.MailDropApiKey) + mailDropClient.DefaultRequestHeaders.Add( + "User-Agent", + "dusted.codes") + let postMail = Http.postJson mailDropClient + let saveMail = + Messages.save + postMail settings.General.EnvName settings.Mail.Domain settings.Mail.Sender settings.Mail.Recipient - pubSubClient let getReportFunc = GoogleAnalytics.getMostViewedPagesAsync settings.ThirdParties.AnalyticsKey services - .AddHttpClient(Http.clientName) - .AddOutgoingGoogleTraceHandler().Services - .AddGoogleTrace(fun x -> x.ProjectId <- settings.GCP.ProjectId) + .AddHttpClient() .AddSingleton(settings) .AddSingleton(getReportFunc) - .AddSingleton(Messages.save saveEntityFunc publishMsgFunc) + .AddSingleton(validateCaptcha) + .AddSingleton(saveMail) .When( settings.Redis.Enabled, fun svc -> svc.AddRedisCache(settings.Redis.Configuration, settings.Redis.Instance)) @@ -64,7 +64,6 @@ module Program = let configureApp (settings : Config.Settings) = fun (app : IApplicationBuilder) -> app.UseErrorHandler() - .UseGoogleTrace() .UseGoogleRequestLogging(settings.Web.RequestLogging) .UseRealIPAddress(settings.Proxy.FwdIPHeaderName, settings.Proxy.ProxyCount) .UseTrailingSlashRedirection(settings.Https.HttpsPort) @@ -82,53 +81,49 @@ module Program = [] let main args = + let log = + Log.write + Log.consoleFormat + [] + Level.Debug + None + "" + "" try - let log = - Log.write - Log.consoleFormat - [] - Level.Debug - None - "" - "" - try - DotEnv.load log - let settings = Config.loadSettings() - log Level.Debug (settings.ToString()) + DotEnv.load log + let settings = Config.loadSettings() + log Level.Debug (settings.ToString()) - let blogPosts = BlogPosts.load Config.blogPostsPath - let lastBlogPost = - blogPosts - |> List.sortByDescending (fun t -> t.PublishDate) - |> List.head + let blogPosts = BlogPosts.load Config.blogPostsPath + let lastBlogPost = + blogPosts + |> List.sortByDescending (fun t -> t.PublishDate) + |> List.head - log Level.Info (sprintf "Parsed %i blog posts." blogPosts.Length) - log Level.Info (sprintf "Last blog post is: %s." lastBlogPost.Title) + log Level.Info (sprintf "Parsed %i blog posts." blogPosts.Length) + log Level.Info (sprintf "Last blog post is: %s." lastBlogPost.Title) - BlogPosts.all <- blogPosts + BlogPosts.all <- blogPosts - Host.CreateDefaultBuilder(args) - .ConfigureWebHost( - fun webHostBuilder -> - webHostBuilder - .ConfigureSentry( - settings.ThirdParties.SentryDsn, - settings.General.AppName, - settings.General.AppVersion) - .UseKestrel( - fun k -> k.AddServerHeader <- false) - .UseContentRoot(Config.appRoot) - .UseWebRoot(Config.assetsPath) - .Configure(configureApp settings) - .ConfigureServices(configureServices settings) - |> ignore) - .Build() - .Run() - 0 - with ex -> - log Level.Emergency - (sprintf "Host terminated unexpectedly: %s\n\nStacktrace: %s" ex.Message ex.StackTrace) - 1 - finally - if isNotNull pubSubClient - then pubSubClient.ShutdownAsync(TimeSpan.FromSeconds 10.0).Wait() \ No newline at end of file + Host.CreateDefaultBuilder(args) + .ConfigureWebHost( + fun webHostBuilder -> + webHostBuilder + .ConfigureSentry( + settings.ThirdParties.SentryDsn, + settings.General.AppName, + settings.General.AppVersion) + .UseKestrel( + fun k -> k.AddServerHeader <- false) + .UseContentRoot(Config.appRoot) + .UseWebRoot(Config.assetsPath) + .Configure(configureApp settings) + .ConfigureServices(configureServices settings) + |> ignore) + .Build() + .Run() + 0 + with ex -> + log Level.Emergency + (sprintf "Host terminated unexpectedly: %s\n\nStacktrace: %s" ex.Message ex.StackTrace) + 1 \ No newline at end of file From 6a7565472e5857a1f48e4f3f6935648b5a1ce4a9 Mon Sep 17 00:00:00 2001 From: Dustin Moris Gorski Date: Mon, 12 Apr 2021 22:23:43 +0100 Subject: [PATCH 2/7] Parser fix --- src/DustedCodes/BlogPosts.fs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/DustedCodes/BlogPosts.fs b/src/DustedCodes/BlogPosts.fs index 13d8f77..ef22426 100644 --- a/src/DustedCodes/BlogPosts.fs +++ b/src/DustedCodes/BlogPosts.fs @@ -162,6 +162,7 @@ module BlogPosts = let private getAllBlogPostsFromDisk (blogPostsPath : string) = blogPostsPath |> Directory.GetFiles + |> Array.filter (fun f -> f.EndsWith ".md") |> Array.map parseBlogPost |> Array.fold (fun (blogPosts, errors) result -> match result with From 6a8ab86948d737ed26517cf549459bb5ae78f698 Mon Sep 17 00:00:00 2001 From: Dustin Moris Gorski Date: Mon, 12 Apr 2021 22:26:18 +0100 Subject: [PATCH 3/7] CDN change --- .../2015_02_26-php-uk-conference-2015.md | 4 +- ...some-awesome-using-icons-without-i-tags.md | 4 +- ...net-mvc-5-error-pages-and-error-logging.md | 2 +- ...n-the-same-microsoft-azure-subscription.md | 12 +- ...ryption-rsa-crash-course-for-developers.md | 6 +- ...er-vs-rsacng-and-good-practise-patterns.md | 2 +- ...features-in-aspdotnet-mvc-5-razor-views.md | 4 +- ...r-or-travisci-builds-with-an-svg-widget.md | 4 +- .../2015_09_28-death-of-a-qa-in-scrum.md | 2 +- ...ument-restful-apis-using-raml-in-dotnet.md | 24 ++-- ...devices-with-google-chrome-bookmarklets.md | 8 +- ...d-and-run-dotnet-applications-in-docker.md | 36 ++--- ...arts-and-nuget-badges-by-buildstatsinfo.md | 2 +- ...ication-in-aspnet-mvc-5-with-powershell.md | 2 +- ...tering-the-aws-service-health-dashboard.md | 4 +- ...ulu-and-more-from-anywhere-in-the-world.md | 28 ++-- ...tom-error-handling-and-logging-in-suave.md | 2 +- ...fsharp-and-suave-in-less-than-5-minutes.md | 6 +- ...-application-with-jmeter-and-amazon-ec2.md | 6 +- ...re-application-with-docker-and-travisci.md | 8 +- ...ve-in-aspnet-core-and-on-top-of-kestrel.md | 8 +- ...017_01_19-error-handling-in-aspnet-core.md | 2 +- ...4-thank-you-microsoft-for-being-awesome.md | 10 +- .../2017_02_07-functional-aspnet-core.md | 4 +- ...et-core-part-2-hello-world-from-giraffe.md | 22 +-- .../2018_02_09-announcing-giraffe-100.md | 2 +- .../2018_10_08-open-source-documentation.md | 4 +- ...-actions-for-dotnet-core-nuget-packages.md | 34 ++--- .../2020_07_22-dotnet-for-beginners.md | 40 +++--- src/DustedCodes/CSS/fonts.css | 136 +++++++++--------- 30 files changed, 214 insertions(+), 214 deletions(-) diff --git a/src/DustedCodes/BlogPosts/2015_02_26-php-uk-conference-2015.md b/src/DustedCodes/BlogPosts/2015_02_26-php-uk-conference-2015.md index 0bc3012..c9db847 100644 --- a/src/DustedCodes/BlogPosts/2015_02_26-php-uk-conference-2015.md +++ b/src/DustedCodes/BlogPosts/2015_02_26-php-uk-conference-2015.md @@ -7,13 +7,13 @@

Last week was my first time at the PHP UK Conference in London. As a .NET developer who is very new to the PHP community I didn't have any particular expectations, but I think this year was a great time to be there!

The Venue

-PHP UK Conference 2015 - The Venue, Image by Dustin Moris GorskiPHP UK Conference 2015 - Open Bar, Image by Dustin Moris Gorski +PHP UK Conference 2015 - The Venue, Image by Dustin Moris GorskiPHP UK Conference 2015 - Open Bar, Image by Dustin Moris Gorski

The conference took place on Thursday and Friday (apparently for the first time, because in previous years it was Friday and Saturday) at The Brewery, which is a great venue at a very central location in the city of London.

Also worth noting is that this year was the 10th anniversary of the event with a record high of more than 700 attendees coming from many different countries around the world. The hosting was excellent, food and drinks were available throughout the entire day and in the evenings they had many hundreds of free beer up for grab to celebrate this occasion.

As if this was not enough, the organisers even rented out the All Star Lanes in Brick lane to continue the celebration with some free bowling, free karaoke, more beer, more food and a cake on Thursday night.

I have to admit that due to a light cold I didn't make the most out of it, but I still had a fantastic time!

The Tracks

-PHP UK Conference 2015 - Closing Keynote, Image by Dustin Moris GorskiPHP UK Conference 2015 - Opening Keynote, Image by Dustin Moris Gorski +PHP UK Conference 2015 - Closing Keynote, Image by Dustin Moris GorskiPHP UK Conference 2015 - Opening Keynote, Image by Dustin Moris Gorski

Both days started off with two great key notes. The first keynote was by coderabbi, who walked us through some of his own experiences, spoke about code reviews and peer coding and eventually spread his wisdom over a packed room.

On Friday Jenny Wong, a developer and (according to her own words) a "community junkie" kicked off the second day with a very light hearted and extremely inspirational speech about bringing developer communities together.

Integrating Communities

diff --git a/src/DustedCodes/BlogPosts/2015_03_04-making-font-awesome-awesome-using-icons-without-i-tags.md b/src/DustedCodes/BlogPosts/2015_03_04-making-font-awesome-awesome-using-icons-without-i-tags.md index b746b85..e5a865e 100644 --- a/src/DustedCodes/BlogPosts/2015_03_04-making-font-awesome-awesome-using-icons-without-i-tags.md +++ b/src/DustedCodes/BlogPosts/2015_03_04-making-font-awesome-awesome-using-icons-without-i-tags.md @@ -54,9 +54,9 @@
<a class="fa-car" href="#">This is a link</a>

Result:
This is a link

The icon isn't what we want, but at least the tag's original font remains as is. Using the Google Chrome developer tools I can quickly confirm that the icon-specific class is not doing any harm to the original tag:

-CSS source code of the Font Awesome car icon, Image by Dustin Moris Gorski +CSS source code of the Font Awesome car icon, Image by Dustin Moris Gorski

Evidently this class only adds the content to the ::before attribute of the target element. The conclusion is that the actual styling gets applied via the "fa" class:

-CSS source code of the Font Awesome fa class, Image by Dustin Moris Gorski +CSS source code of the Font Awesome fa class, Image by Dustin Moris Gorski

Now this makes sense. The content from the ::before attribute gets rendered inside the original tag and therefore also picks up the styling from the fa class.

Everything from the fa class could equally go into the icon class as part of the ::before attribute, but I can see why the Font Awesome team has extracted it into a shared class, because it is the same for every icon and would be otherwise a maintenance nightmare.

diff --git a/src/DustedCodes/BlogPosts/2015_04_06-demystifying-aspnet-mvc-5-error-pages-and-error-logging.md b/src/DustedCodes/BlogPosts/2015_04_06-demystifying-aspnet-mvc-5-error-pages-and-error-logging.md index ebd0291..fae6426 100644 --- a/src/DustedCodes/BlogPosts/2015_04_06-demystifying-aspnet-mvc-5-error-pages-and-error-logging.md +++ b/src/DustedCodes/BlogPosts/2015_04_06-demystifying-aspnet-mvc-5-error-pages-and-error-logging.md @@ -41,7 +41,7 @@

ASP.NET itself is a larger framework to process incoming requests. Even though it could handle incoming requests from different sources, it is almost exclusively used with IIS. It can be extended with HttpModules and HttpHandlers.

HttpModules are plugged into the pipeline to process a request at any point of the ASP.NET life cycle. A HttpHandler is responsible for producing a response/output for a request.

IIS (Microsoft's web server technology) will create an incoming request for ASP.NET, which subsequently will start processing the request and eventually initialize the HttpApplication (which is the default handler) and create a response:

-IIS, ASP.NET and MVC architecture, Image by Dustin Moris Gorski +IIS, ASP.NET and MVC architecture, Image by Dustin Moris Gorski

The key thing to know is that ASP.NET can only handle requests which IIS forwards to it. This is determined by the registered HttpHandlers (e.g. by default a request to a .htm file is not handled by ASP.NET).

And finally, MVC is only one of potentially many registered handlers in the ASP.NET pipeline.

This is crucial to understand the impact of different error handling methods.

diff --git a/src/DustedCodes/BlogPosts/2015_06_14-running-free-tier-and-paid-tier-web-apps-on-the-same-microsoft-azure-subscription.md b/src/DustedCodes/BlogPosts/2015_06_14-running-free-tier-and-paid-tier-web-apps-on-the-same-microsoft-azure-subscription.md index ebcbcbb..aab2c60 100644 --- a/src/DustedCodes/BlogPosts/2015_06_14-running-free-tier-and-paid-tier-web-apps-on-the-same-microsoft-azure-subscription.md +++ b/src/DustedCodes/BlogPosts/2015_06_14-running-free-tier-and-paid-tier-web-apps-on-the-same-microsoft-azure-subscription.md @@ -7,26 +7,26 @@

Last week I noticed a charge of ~ £20 by MSFT AZURE on my bank statement and initially struggled to work out why I was charged this much.

I knew I'd have to pay something for this website, which is hosted on the shared tier in Microsoft Azure, but according to Microsoft Azure's pricing calculator it should have only come to £5.91 per month:

-Windows Azure Shared Pricing Tier, Image by Dustin Moris Gorski +Windows Azure Shared Pricing Tier, Image by Dustin Moris Gorski

After a little investigation I quickly found the issue, it was due to a few on and off test web apps which were running on the shared tier as well.

This was clearly a mistake, because I was confident that I created all my test apps on the free tier, but as it turned out, after I upgraded my production website to the shared tier all my other newly created apps were running on the shared tier as well.

I simply didn't pay close attention during the creation process:

-Windows Azure Create new Web App, Image by Dustin Moris Gorski +Windows Azure Create new Web App, Image by Dustin Moris Gorski

Evidentially every new web app gets automatically assigned to my existing app service plan, which I upgraded to the shared tier.

Luckily I learned my lesson after the first bill. However my initial attempt to switch my test apps back to the free tier was not as simple as I thought it would be. I cannot scale one app individually without affecting all other apps on the same plan:

-Windows Azure change pricing tier, Image by Dustin Moris Gorski +Windows Azure change pricing tier, Image by Dustin Moris Gorski

The solution is to create a new app service plan and assign it to the free tier.

You can do this either when creating a new web app, by picking "Create new App Service plan" from the drop down:

-Windows Azure Create new App Service plan, Image by Dustin Moris Gorski +Windows Azure Create new App Service plan, Image by Dustin Moris Gorski

Or when navigating to the new Portal, where you have the possibility to manage your app service plans:

-Windows Azure switch to Azure Preview Portal, Image by Dustin Moris Gorski -Windows Azure New Portal App Service Plans Menu, Image by Dustin Moris Gorski +Windows Azure switch to Azure Preview Portal, Image by Dustin Moris Gorski +Windows Azure New Portal App Service Plans Menu, Image by Dustin Moris Gorski

This wasn't difficult at all, but certainly a mistake which can easily happen to anyone who is new to Microsoft Azure.

Another very useful thing to know is that if you choose the same data centre location for all your app service plans, then you can easily move a web app from one plan to another. This could be very handy when having different test and/or production stages (Dev/Staging/Production).

\ No newline at end of file diff --git a/src/DustedCodes/BlogPosts/2015_06_28-the-beauty-of-asymmetric-encryption-rsa-crash-course-for-developers.md b/src/DustedCodes/BlogPosts/2015_06_28-the-beauty-of-asymmetric-encryption-rsa-crash-course-for-developers.md index 682cf4d..a8359a6 100644 --- a/src/DustedCodes/BlogPosts/2015_06_28-the-beauty-of-asymmetric-encryption-rsa-crash-course-for-developers.md +++ b/src/DustedCodes/BlogPosts/2015_06_28-the-beauty-of-asymmetric-encryption-rsa-crash-course-for-developers.md @@ -34,7 +34,7 @@

Alice, Bob and Eve

Alice and Bob want to communicate privately and Eve wants to eavesdrop. Both, Alice and Bob have their individual public and private key pair.

Alice uses Bob's public key to encrypt a private message before sending it to Bob. Bob can use his private key to decrypt the message. Now Bob can use Alice's public key to reply to Alice without Eve being able to understand any of the transmitted data. Finally Alice decrypts Bob's message with her own private key.

-Public Key Encryption, Image by Dustin Moris Gorski +Public Key Encryption, Image by Dustin Moris Gorski

The public key is available to everyone, while the private key is only known to the key holder. There is never the requirement to share a secret key via an insecure channel.

Integrity and Authenticity

@@ -54,13 +54,13 @@

A simple example of a one-way function

Let's say the initial value is 264. The one-way function reads as following:

You start from the centre of a map. Now take your value and divide it by it's last digit. The result is a new value x. Now draw a line x centimetres north east and mark a new point on the map. Next take your original value and subtract it by x. The result is y. Draw another line, starting from the last point, y centimetres south west. The final point is the end result.

-Example of a one way function, Image by Dustin Moris Gorski +Example of a one way function, Image by Dustin Moris Gorski

In this example we would divide 264 by 4 and retrieve 66 for x. Additionally we subtract 66 from 264 and retrieve y = 198. We draw both lines and determine the final point on the map, which represents the end result of the one-way function.

Now just from knowing the final point on the map and the definition of the function it is not possible to easily deduce the original value.

Modular arithmetic

Modular arithmetic is full of one-way functions. It is also known as clock arithmetic, because it can be illustrated by a finite amount of numbers arranged in a loop, like on a clock:

-Clock Arithmetic, Image by Dustin Moris Gorski +Clock Arithmetic, Image by Dustin Moris Gorski

The dark circle represents the clock. The blue numbers represent the value 17. If you arrange all numbers from 1 to 17 clockwise in a loop, then the end value results in 5. In other words 17 mod 12 equals 5.

The short-cut and common way of calculating the modulus is by dividing the original value by x. The reminder equals the modulus.

The modulus operation is a great one-way function, because it is fairly simple and has an infinite amount of possible values giving the same result.

diff --git a/src/DustedCodes/BlogPosts/2015_08_13-how-to-use-rsa-in-dotnet-rsacryptoserviceprovider-vs-rsacng-and-good-practise-patterns.md b/src/DustedCodes/BlogPosts/2015_08_13-how-to-use-rsa-in-dotnet-rsacryptoserviceprovider-vs-rsacng-and-good-practise-patterns.md index 3bfdb54..f0a3f4c 100644 --- a/src/DustedCodes/BlogPosts/2015_08_13-how-to-use-rsa-in-dotnet-rsacryptoserviceprovider-vs-rsacng-and-good-practise-patterns.md +++ b/src/DustedCodes/BlogPosts/2015_08_13-how-to-use-rsa-in-dotnet-rsacryptoserviceprovider-vs-rsacng-and-good-practise-patterns.md @@ -45,7 +45,7 @@
  • public virtual byte[] Decrypt(byte[] data, RSAEncryptionPadding padding)
  • Interestingly they are not mentioned in the official MSDN documentation on the web, however when I decompile .NET 4.0's mscorlib I can see the two virtual methods:

    -Encrypt and Decrypt methods in .NET C# RSA Class, Image by Dustin Moris Gorski +Encrypt and Decrypt methods in .NET C# RSA Class, Image by Dustin Moris Gorski

    This was a great addition for two reasons in particular:

      diff --git a/src/DustedCodes/BlogPosts/2015_08_21-using-csharp-6-features-in-aspdotnet-mvc-5-razor-views.md b/src/DustedCodes/BlogPosts/2015_08_21-using-csharp-6-features-in-aspdotnet-mvc-5-razor-views.md index 151f4b7..597162d 100644 --- a/src/DustedCodes/BlogPosts/2015_08_21-using-csharp-6-features-in-aspdotnet-mvc-5-razor-views.md +++ b/src/DustedCodes/BlogPosts/2015_08_21-using-csharp-6-features-in-aspdotnet-mvc-5-razor-views.md @@ -8,7 +8,7 @@

      Recently I upgraded my IDE to Visual Studio 2015 and made instant use of many new C# 6 features like the nameof keyword or interpolated strings.

      It worked (and compiled) perfectly fine until I started using C# 6 features in ASP.NET MVC 5 razor views:

      -Feature not available in C# 5 message, Image by Dustin Moris Gorski +Feature not available in C# 5 message, Image by Dustin Moris Gorski

      Feature 'interpolated strings' is not available in C# 5. Please use language version 6 or greater.

      @@ -21,7 +21,7 @@

      However, saying that I don't get any errors at compilation time even though I made a lot of use of C# 6 features all over my project.

      Maybe it is an intellisense bug in Visual Studio 2015? Not really, because when I start my project I get a yellow screen of death which matches the intellisense error:

      -Interpolated String Runtime Error in ASP.NET MVC 5, Image by Dustin Moris Gorski +Interpolated String Runtime Error in ASP.NET MVC 5, Image by Dustin Moris Gorski

      ASP.NET Runtime compiler

      The problem is at runtime when ASP.NET tries to compile the razor view. ASP.NET MVC 5 uses the CodeDOM Provider which doesn't support C# 6 language features. diff --git a/src/DustedCodes/BlogPosts/2015_08_30-display-build-history-charts-for-appveyor-or-travisci-builds-with-an-svg-widget.md b/src/DustedCodes/BlogPosts/2015_08_30-display-build-history-charts-for-appveyor-or-travisci-builds-with-an-svg-widget.md index ced64dd..8fff96f 100644 --- a/src/DustedCodes/BlogPosts/2015_08_30-display-build-history-charts-for-appveyor-or-travisci-builds-with-an-svg-widget.md +++ b/src/DustedCodes/BlogPosts/2015_08_30-display-build-history-charts-for-appveyor-or-travisci-builds-with-an-svg-widget.md @@ -8,10 +8,10 @@

      If you ever browsed a popular GitHub repository (like NUnit or Bootstrap) then you must have seen many of the available SVG badges which can be used to decorate a repository's README file.

      While some repositories keep it very simple:

      -NUnit Project Badges, Image by Dustin Moris Gorski +NUnit Project Badges, Image by Dustin Moris Gorski

      Others can be quite fancy:

      -Bootstrap Project Badges, Image by Dustin Moris Gorski +Bootstrap Project Badges, Image by Dustin Moris Gorski

      These little widgets (or often called badges) are more of a gimmick rather than anything useful, but we love them because they give us an opportunity to visually highlight statistics or achievements which we are proud of.

      diff --git a/src/DustedCodes/BlogPosts/2015_09_28-death-of-a-qa-in-scrum.md b/src/DustedCodes/BlogPosts/2015_09_28-death-of-a-qa-in-scrum.md index 3167756..77e13b1 100644 --- a/src/DustedCodes/BlogPosts/2015_09_28-death-of-a-qa-in-scrum.md +++ b/src/DustedCodes/BlogPosts/2015_09_28-death-of-a-qa-in-scrum.md @@ -35,7 +35,7 @@

      What was happening is that a user story got divided into several work tasks and each task was worked on by a different person in the team. It felt a lot like a production line:

      -Scrum User Story Production Line, Image by Dustin Moris Gorski +Scrum User Story Production Line, Image by Dustin Moris Gorski

      While it was possible to do some parallel work on the development and QA task at the same time, it was not possible to close one before the other. We had inter team dependencies.

      diff --git a/src/DustedCodes/BlogPosts/2015_12_23-design-test-and-document-restful-apis-using-raml-in-dotnet.md b/src/DustedCodes/BlogPosts/2015_12_23-design-test-and-document-restful-apis-using-raml-in-dotnet.md index c86da37..16292b4 100644 --- a/src/DustedCodes/BlogPosts/2015_12_23-design-test-and-document-restful-apis-using-raml-in-dotnet.md +++ b/src/DustedCodes/BlogPosts/2015_12_23-design-test-and-document-restful-apis-using-raml-in-dotnet.md @@ -328,33 +328,33 @@ Now that I have a detailed specification of what my API should look like it is t First I create an empty test project and include the RAML file (api.raml) in a solution folder to keep everything together: -RAML-Demo-Solution-Tree, Image by Dustin Moris Gorski +RAML-Demo-Solution-Tree, Image by Dustin Moris Gorski For the next part I have to install the [RAML Tools for .NET](https://github.com/mulesoft-labs/raml-dotnet-tools) Visual Studio extension: -RAML-Demo-Visual-Studio-RAML-Extension, Image by Dustin Moris Gorski +RAML-Demo-Visual-Studio-RAML-Extension, Image by Dustin Moris Gorski After a successful install I have an additional context menu when I right click the "References" item underneath my test project: -RAML-Demo-Add-RAML-Reference, Image by Dustin Moris Gorski +RAML-Demo-Add-RAML-Reference, Image by Dustin Moris Gorski A click on that menu item pops up a pretty much self-explaining dialog: -RAML-Demo-Add-RAML-Reference-Dialog, Image by Dustin Moris Gorski +RAML-Demo-Add-RAML-Reference-Dialog, Image by Dustin Moris Gorski I select the Upload option and navigate to the api.raml inside my solution folder. After confirmation I am presented with an Import RAML dialog: -RAML-Demo-Create-Client, Image by Dustin Moris Gorski +RAML-Demo-Create-Client, Image by Dustin Moris Gorski The import process automatically detected my single endpoint and the only thing I had to change was the default client name to "ParcelDeliveryApiClient" in case I want to import another API at a later point. Hitting the Import button finishes the remaining work and once completed I am seeing a new API reference in my project tree: -RAML-Demo-RAML-References-in-Project, Image by Dustin Moris Gorski +RAML-Demo-RAML-References-in-Project, Image by Dustin Moris Gorski This was a very smooth and painless process and if successfully imported I should be able to create an instance of `ParcelDeliveryApiClient` in a new class file: -RAML-Demo-Aut-Generated-Client-in-Code, Image by Dustin Moris Gorski +RAML-Demo-Aut-Generated-Client-in-Code, Image by Dustin Moris Gorski Amazing, let's explore the auto-generated client by writing some tests in the next step! @@ -521,21 +521,21 @@ Among many other features [Anypoint](https://anypoint.mulesoft.com/) allows me t The designer is exceptionally well done. It offers many features like syntax highlighting, intellisense, instant RAML validation and auto-suggestion of available nodes: -RAML-Demo-Anypoint-Designer-Editor, Image by Dustin Moris Gorski +RAML-Demo-Anypoint-Designer-Editor, Image by Dustin Moris Gorski -RAML-Demo-Anypoint-Designer-Suggested-Nodes, Image by Dustin Moris Gorski +RAML-Demo-Anypoint-Designer-Suggested-Nodes, Image by Dustin Moris Gorski Another brilliant feature is the interactive preview when editing a RAML file. It visually displays every characteristic of your API in a beautiful interface, like those responses as an example: -RAML-Demo-Anypoint-Designer-Preview-Responses, Image by Dustin Moris Gorski +RAML-Demo-Anypoint-Designer-Preview-Responses, Image by Dustin Moris Gorski It even goes as far as allowing me to interact with a mocked service while working on the RAML: -RAML-Demo-Anypoint-Designer-Preview, Image by Dustin Moris Gorski +RAML-Demo-Anypoint-Designer-Preview, Image by Dustin Moris Gorski When I click the Try It button it displays me a form with all relevant parameters pre-populated with the values from the examples in my RAML: -RAML-Demo-Anypoint-Designer-TryIt-Request, Image by Dustin Moris Gorski +RAML-Demo-Anypoint-Designer-TryIt-Request, Image by Dustin Moris Gorski From the UI I can quickly run requests against my API with as little friction as possible. diff --git a/src/DustedCodes/BlogPosts/2015_12_31-diagnosing-css-issues-on-mobile-devices-with-google-chrome-bookmarklets.md b/src/DustedCodes/BlogPosts/2015_12_31-diagnosing-css-issues-on-mobile-devices-with-google-chrome-bookmarklets.md index 6d4df29..b742690 100644 --- a/src/DustedCodes/BlogPosts/2015_12_31-diagnosing-css-issues-on-mobile-devices-with-google-chrome-bookmarklets.md +++ b/src/DustedCodes/BlogPosts/2015_12_31-diagnosing-css-issues-on-mobile-devices-with-google-chrome-bookmarklets.md @@ -20,7 +20,7 @@ With a little bit of Google's help and playing around in the Google Chrome Conso When I execute this in the console it will outline every element on the page: -page-with-outlined-elements, Image by Dustin Moris Gorski +page-with-outlined-elements, Image by Dustin Moris Gorski With this it should be easy to spot the overflowing element. Now I had to find a way to execute it inside the mobile version of Google Chrome. @@ -44,16 +44,16 @@ When I click on the newly created bookmarklet I will get the same result as if I Only seconds later it appeared on my phone as well: -mobile-google-chrome-bookmarks-bar, Image by Dustin Moris Gorski +mobile-google-chrome-bookmarks-bar, Image by Dustin Moris Gorski However, when I click on the bookmarklet from the bookmarks menu on my phone everything freezes and nothing happens. It turns out that I have to execute it from the address bar. Just start typing the name of your bookmarklet and Google Chrome will auto-suggest the item for you: -outline-elements-bookmarklet-in-mobile-google-chrome, Image by Dustin Moris Gorski +outline-elements-bookmarklet-in-mobile-google-chrome, Image by Dustin Moris Gorski Executing it from the address bar delivers the correct result: -page-with-outlined-elements-on-mobile-phone, Image by Dustin Moris Gorski +page-with-outlined-elements-on-mobile-phone, Image by Dustin Moris Gorski This little trick quickly helped me to find the overflowing element on my phone without having to modify the original website. I use the same technique to remove advertising banners and other blocking content on several websites which normally don't display the entire content when you are not logged in (or a paying customer). \ No newline at end of file diff --git a/src/DustedCodes/BlogPosts/2016_01_07-running-nancyfx-in-a-docker-container-a-beginners-guide-to-build-and-run-dotnet-applications-in-docker.md b/src/DustedCodes/BlogPosts/2016_01_07-running-nancyfx-in-a-docker-container-a-beginners-guide-to-build-and-run-dotnet-applications-in-docker.md index 242280b..bf3b089 100644 --- a/src/DustedCodes/BlogPosts/2016_01_07-running-nancyfx-in-a-docker-container-a-beginners-guide-to-build-and-run-dotnet-applications-in-docker.md +++ b/src/DustedCodes/BlogPosts/2016_01_07-running-nancyfx-in-a-docker-container-a-beginners-guide-to-build-and-run-dotnet-applications-in-docker.md @@ -39,23 +39,23 @@ This leaves the Docker Terminal as the last application and the only thing which After a successful installation let's run a first Docker command to see if things generally work. When you open the terminal for the first time it will initialize the VM in VirtualBox. This may take a few seconds but eventually you should end up at a screen like this: -docker-quickstart-terminal, Image by Dustin Moris Gorski +docker-quickstart-terminal, Image by Dustin Moris Gorski You don't have to open Kitematic or VirtualBox to get it running. As I said before, you can happily ignore those two applications, however, if you are curious you can look into VirtualBox and see the VM running as expected: -

      oracle-virtualbox-docker-default-vm-details, Image by Dustin Moris Gorskioracle-virtualbox-docker-default-vm, Image by Dustin Moris Gorski

      +

      oracle-virtualbox-docker-default-vm-details, Image by Dustin Moris Gorskioracle-virtualbox-docker-default-vm, Image by Dustin Moris Gorski

      It's a Linux box loaded from the boot2docker.iso. Back to the terminal I can now type `docker version` to get some basic version information about the Docker client and server application: -docker-version, Image by Dustin Moris Gorski +docker-version, Image by Dustin Moris Gorski With that I am good to go with Docker now. Maybe one thing which is worth mentioning at this point is the initial message in the Docker Terminal: -docker-host-ip-address, Image by Dustin Moris Gorski +docker-host-ip-address, Image by Dustin Moris Gorski The IP address which is shown in the terminal is the endpoint from where you can reach your application later in this tutorial. @@ -222,7 +222,7 @@ public class IndexModule : NancyModule If I compile and run the application then I should be able to see the hello world message when visiting [http://localhost:8888](http://localhost:8888) and see the OS version at [http://localhost:8888/os](http://localhost:8888/os): -

      nancy-hello-world-in-browser, Image by Dustin Moris Gorskinancy-os-version-in-browser, Image by Dustin Moris Gorski

      +

      nancy-hello-world-in-browser, Image by Dustin Moris Gorskinancy-os-version-in-browser, Image by Dustin Moris Gorski

      ## Running NancyFx in a Docker container @@ -234,15 +234,15 @@ First I need to build a Docker image which will contain the entire application a It is good practice to add the Dockerfile into your project folder, because it may change when your project changes: -dockerfile-in-project-tree, Image by Dustin Moris Gorski +dockerfile-in-project-tree, Image by Dustin Moris Gorski I also want to include the Dockerfile in the build output, therefore I have to change the "Build Action" setting to "Content" and "Copy to Output Directory" to "Copy always": -dockerfile-properties, Image by Dustin Moris Gorski +dockerfile-properties, Image by Dustin Moris Gorski Visual Studio 2015 creates text files with [UTF-8-BOM encoding](http://stackoverflow.com/questions/2223882/whats-different-between-utf-8-and-utf-8-without-bom) by default. This adds an additional (invisible) BOM character at the very beginning of the text file and will cause an error when trying to build an image from the Dockerfile. The easiest way to change this is by opening the file in [Notepad++](https://notepad-plus-plus.org/) and changing the encoding to UTF-8 (without BOM): -dockerfile-encoding, Image by Dustin Moris Gorski +dockerfile-encoding, Image by Dustin Moris Gorski *You can also [permanently change Visual Studio to save files without BOM](http://stackoverflow.com/questions/5406172/utf-8-without-bom#answer-5411486).* @@ -260,7 +260,7 @@ Fortunately there is already an [official Mono repository](https://hub.docker.co If you look at the [official Mono repository](https://hub.docker.com/_/mono/) you can see that the latest Mono image has multiple tags: -mono-latest-image-tag, Image by Dustin Moris Gorski +mono-latest-image-tag, Image by Dustin Moris Gorski It depends on your use case which tag makes the most sense for your application. Currently they all have been built from the same Dockerfile, but only tag `4.2.1.102` is explicit enough to always guarantee the exact same build. Personally I would chose this one for a production application: @@ -309,7 +309,7 @@ Don't forget the dot at the end. This is the path to the directory which contain The build process will go through each instruction and create a new layer after executing it. The first time you build an image you are likely not going to have the `mono:4.2.1.102` image on disk and Docker will pull it from the public registry (Docker Hub): -docker-build-command, Image by Dustin Moris Gorski +docker-build-command, Image by Dustin Moris Gorski As you can see the FROM instruction requires Docker to download 6 different images. This is because the `mono:4.2.1.102` image and all of its ancestors (`debian:wheezy`) have 6 instructions in total, which result in 6 layered images. @@ -317,11 +317,11 @@ A better way of visualizing this is by inspecting our own image. Once the build is complete we can list all available images with the `docker images` command: -docker-images-command, Image by Dustin Moris Gorski +docker-images-command, Image by Dustin Moris Gorski With `docker history {image-id}` I can see the entire history of the image, each layer it is made of and the command which is responsible for the layer: -docker-history, Image by Dustin Moris Gorski +docker-history, Image by Dustin Moris Gorski This is quite clever! Anyway, I am getting carried away here, the point is we just created our first Docker image! @@ -338,15 +338,15 @@ The `-d` option tells Docker to run the container in detached mode and the `-p 8 Afterwards you can run `docker ps` to list all currently running containers: -docker-ps, Image by Dustin Moris Gorski +docker-ps, Image by Dustin Moris Gorski Great, now pasting `{docker-ip}:8888` (the IP address from the beginning) into a browser should return the Nancy hello world message: -nancy-hello-world-in-browser-from-docker-container, Image by Dustin Moris Gorski +nancy-hello-world-in-browser-from-docker-container, Image by Dustin Moris Gorski And going to `{docker-ip}:8888/os` should return "Unix 4.1.13.2": -nancy-os-version-in-browser-from-docker-container, Image by Dustin Moris Gorski +nancy-os-version-in-browser-from-docker-container, Image by Dustin Moris Gorski This is pretty awesome. With almost no effort we managed to run a Nancy .NET application on Mono in a Docker container! @@ -360,7 +360,7 @@ You can map the Docker IP address to a friendly DNS by editing your Windows host Now you can type `docker.local:8888` into your browser and get the same result: -docker-local-host-resolution, Image by Dustin Moris Gorski +docker-local-host-resolution, Image by Dustin Moris Gorski ## Configure environment specific settings with Docker @@ -405,11 +405,11 @@ This has many advantages: After launching the container with the secret setting I can run `docker inspect {container-id}` to load a whole bunch of information on the container. One piece of information is the environment variables which have been loaded for that container: -docker-inspect-env-vars, Image by Dustin Moris Gorski +docker-inspect-env-vars, Image by Dustin Moris Gorski Going to [docker.local:8888/secret](http://docker.local:8888/secret) will expose the secret environment variable now: -docker-secret-in-browser, Image by Dustin Moris Gorski +docker-secret-in-browser, Image by Dustin Moris Gorski ## Recap diff --git a/src/DustedCodes/BlogPosts/2016_02_29-circleci-build-history-charts-and-nuget-badges-by-buildstatsinfo.md b/src/DustedCodes/BlogPosts/2016_02_29-circleci-build-history-charts-and-nuget-badges-by-buildstatsinfo.md index acd99af..e527954 100644 --- a/src/DustedCodes/BlogPosts/2016_02_29-circleci-build-history-charts-and-nuget-badges-by-buildstatsinfo.md +++ b/src/DustedCodes/BlogPosts/2016_02_29-circleci-build-history-charts-and-nuget-badges-by-buildstatsinfo.md @@ -14,7 +14,7 @@ On a complete separate note I also added a new SVG badge for [NuGet packages](ht I did not think of adding NuGet support in the beginning, but since [Shields.io](http://shields.io/) NuGet badges are broken for more than 2 weeks now I had to look for an alternative: -shields.io-broken-nuget-badges, Image by Dustin Moris Gorski +shields.io-broken-nuget-badges, Image by Dustin Moris Gorski The [issue has been reported](https://github.com/badges/shields/issues/655), but it doesn't look like it will get fixed any time soon and so I went with my own solution. diff --git a/src/DustedCodes/BlogPosts/2016_03_26-automating-css-and-javascript-minification-in-aspnet-mvc-5-with-powershell.md b/src/DustedCodes/BlogPosts/2016_03_26-automating-css-and-javascript-minification-in-aspnet-mvc-5-with-powershell.md index e6bbc02..a236a0c 100644 --- a/src/DustedCodes/BlogPosts/2016_03_26-automating-css-and-javascript-minification-in-aspnet-mvc-5-with-powershell.md +++ b/src/DustedCodes/BlogPosts/2016_03_26-automating-css-and-javascript-minification-in-aspnet-mvc-5-with-powershell.md @@ -133,7 +133,7 @@ Go to the "Build Events" dialog and paste the following code into the %windir%\System32\WindowsPowerShell\v1.0\powershell.exe -NoLogo -NonInteractive -Command "$(SolutionDir)MinifyJavaScript.ps1" $(SolutionDir) ) -aspnet-mvc-5-post-build-event-command-line, Image by Dustin Moris Gorski +aspnet-mvc-5-post-build-event-command-line, Image by Dustin Moris Gorski This code block makes sure that we only execute the PowerShell scripts when the project doesn't build in Debug mode. This is desired because during development we might make frequent changes to the original CSS file and not want to minify the content until we are ready to build in Release mode. diff --git a/src/DustedCodes/BlogPosts/2016_03_31-filtering-the-aws-service-health-dashboard.md b/src/DustedCodes/BlogPosts/2016_03_31-filtering-the-aws-service-health-dashboard.md index 1ca18db..ee9ee94 100644 --- a/src/DustedCodes/BlogPosts/2016_03_31-filtering-the-aws-service-health-dashboard.md +++ b/src/DustedCodes/BlogPosts/2016_03_31-filtering-the-aws-service-health-dashboard.md @@ -8,12 +8,12 @@ If you run anything on [Amazon Web Services](https://aws.amazon.com/) in product Sometimes when you experience a service disruption you might find yourself scrolling through the page and look for an update for one of your affected resources and then you probably wish that the icons were a little bit more distinguishable between healthy and previously unhealthy services: -aws-service-health-dashboard, Image by Dustin Moris Gorski +aws-service-health-dashboard, Image by Dustin Moris Gorski Unless you have perfect eagle sight you probably struggle to quickly filter the page with your plain eye and could use some help to better visualise good from bad icons. At least this is how I feel and therefore I created this little JavaScript snippet to blend out healthy icons from the page, leaving only the bad ones visible:
      [].forEach.call(document.querySelectorAll('[src="/images/status0.gif"]'), function(e) {e.style.display = "none";});
      -aws-service-health-dashboard-filtered, Image by Dustin Moris Gorski +aws-service-health-dashboard-filtered, Image by Dustin Moris Gorski You can either copy this into your browser's console and run it directly from there or [create a permanent bookmarklet](https://dusted.codes/diagnosing-css-issues-on-mobile-devices-with-google-chrome-bookmarklets) for easy access in the future. \ No newline at end of file diff --git a/src/DustedCodes/BlogPosts/2016_05_01-watch-us-netflix-hulu-and-more-from-anywhere-in-the-world.md b/src/DustedCodes/BlogPosts/2016_05_01-watch-us-netflix-hulu-and-more-from-anywhere-in-the-world.md index 95c558b..ae7b4f6 100644 --- a/src/DustedCodes/BlogPosts/2016_05_01-watch-us-netflix-hulu-and-more-from-anywhere-in-the-world.md +++ b/src/DustedCodes/BlogPosts/2016_05_01-watch-us-netflix-hulu-and-more-from-anywhere-in-the-world.md @@ -47,15 +47,15 @@ Once signed in you should see a list of available AWS Services. We are going to Click on the EC2 link from the menu: -ec2-menu-item, Image by Dustin Moris Gorski +ec2-menu-item, Image by Dustin Moris Gorski In the top right corner make sure you have selected a US region, because after all we want our proxy to stream US content: -aws-select-us-region-from-dropdown, Image by Dustin Moris Gorski +aws-select-us-region-from-dropdown, Image by Dustin Moris Gorski Next click on the **Launch Instance** button: -aws-launch-instance-button, Image by Dustin Moris Gorski +aws-launch-instance-button, Image by Dustin Moris Gorski This opens up a 7-step wizard which will walk you through the configuration of a new EC2 instance. Don't worry, there is not much that needs to be done to get the proxy server up and running. @@ -63,19 +63,19 @@ The first step lets you choose which image (AMI) to use for your new instance. A For the purpose of the proxy server we don't need anything fancy and therefore can go with the **Ubuntu Server**, which is a free tier eligible Linux distribution: -aws-ubuntu-free-tier-ami, Image by Dustin Moris Gorski +aws-ubuntu-free-tier-ami, Image by Dustin Moris Gorski On the next screen you can pick the size of the new instance. If you don't know what this means, think of it like the horse power of your new server. Again, because we don't need anything fancy we can happily go with the **t2.micro** instance which is free tier eligible: -aws-t2-micro-free-tier-instance, Image by Dustin Moris Gorski +aws-t2-micro-free-tier-instance, Image by Dustin Moris Gorski Don't click on the *Review and Launch* button yet! Confirm and continue by clicking on the **Next: Configure Instance Details** button. On the third step there's a bunch of information available, but luckily the default values are exactly what we need and you don't have to change any of them, except one thing. Scroll down to the bottom and expand the **Advanced Details** section by clicking on the little arrow next to it: -aws-ec2-instance-advanced-details, Image by Dustin Moris Gorski +aws-ec2-instance-advanced-details, Image by Dustin Moris Gorski You will be presented with a text field which can be used to specify additional commands which will run when launching the new EC2 instance. We will add a few commands which will automatically install and configure the [Tinyproxy](https://tinyproxy.github.io/) software. Tinyproxy is a [free and open source proxy server](https://github.com/tinyproxy/tinyproxy) for POSIX operating systems. @@ -94,7 +94,7 @@ This is an important step, because by default Tinyproxy does not allow any conne The end result should look something like this, except with your own IP address: -aws-ec2-instance-advanced-details-commands, Image by Dustin Moris Gorski +aws-ec2-instance-advanced-details-commands, Image by Dustin Moris Gorski Continue by clicking on the **Next: Add Storage** button. @@ -106,7 +106,7 @@ By default the wizard will create a new security group for you and add one rule In the drop down select **Custom TCP Rule** and enter port **8888** into the **Port Range** field. Why port 8888? Because this is the default port which Tinyproxy listens to. Under **Source** pick the **Custom IP** option and enter your IP address in the field next to it and append "/32" to the end: -aws-create-security-group, Image by Dustin Moris Gorski +aws-create-security-group, Image by Dustin Moris Gorski As you can see in the screen shot I also changed the security group name to something more meaningful. Feel free to pick your own name. @@ -116,11 +116,11 @@ Almost done now! The final step is to create a private key pair. The private key After that you should be able to click the **Launch Instances** button and let Amazon web services do the rest for you: -aws-launch-status, Image by Dustin Moris Gorski +aws-launch-status, Image by Dustin Moris Gorski When you click on the instance id you get redirected back to the EC2 console where you can see your instance being initialized. It may take a few minutes until everthing is ready and once completed you should see the status to be "running" and all status checks to be OK: -aws-ec2-instance-status-and-public-ip, Image by Dustin Moris Gorski +aws-ec2-instance-status-and-public-ip, Image by Dustin Moris Gorski The public IP address that you see with your instance is your new private proxy IP address! Take a note of it, because you will need it in the last step. @@ -143,7 +143,7 @@ Follow these instructions even if you don't use Internet Explorer for browsing t 5. Enter the IP address of your proxy server and port 8888 into the text fields 6. Click OK and confirm everything -windows-proxy-settings, Image by Dustin Moris Gorski +windows-proxy-settings, Image by Dustin Moris Gorski

      Change proxy server settings in Android

      @@ -156,7 +156,7 @@ Follow these instructions even if you don't use Internet Explorer for browsing t 7. Type in the IP address of your proxy server into the Proxy hostname field and port 8888 into Proxy port 8. Save those settings -android-proxy-settings, Image by Dustin Moris Gorski +android-proxy-settings, Image by Dustin Moris Gorski

      Change proxy server settings in iOS

      @@ -167,13 +167,13 @@ Follow these instructions even if you don't use Internet Explorer for browsing t 5. Type in the IP address of your proxy server into the Server field 6. Type 8888 into the Port field -ios-proxy-settings, Image by Dustin Moris Gorski +ios-proxy-settings, Image by Dustin Moris Gorski ## Test your connection Now that everything is set up and running you should be able to stream US content from Netflix, Hulu and many more! A quick test to confirm that your proxy server is successfully running would be to [google your own IP address](https://www.iplocation.net/find-ip-address) and see that your IP appears different, from a US location now: -proxy-server-active, Image by Dustin Moris Gorski +proxy-server-active, Image by Dustin Moris Gorski And what shall you do after your first 12 months of free tier eligibility? Well, I'd suggest you sign up a new account under a different email address. It takes only a couple of minutes which you have to invest every 12 months in order to run a free private proxy server. If that is too much effort then you might as well choose to let your server run and pay for its usage. The t2.micro instance costs only [$0.013 per hour](https://aws.amazon.com/ec2/pricing/) which would come down to $9.75 per month if you'd let it run continuously. However, in this case I'd suggest to switch it on and off as you need and reduce your cost to almost nothing. diff --git a/src/DustedCodes/BlogPosts/2016_05_31-custom-error-handling-and-logging-in-suave.md b/src/DustedCodes/BlogPosts/2016_05_31-custom-error-handling-and-logging-in-suave.md index 95223ec..01cdd9c 100644 --- a/src/DustedCodes/BlogPosts/2016_05_31-custom-error-handling-and-logging-in-suave.md +++ b/src/DustedCodes/BlogPosts/2016_05_31-custom-error-handling-and-logging-in-suave.md @@ -8,7 +8,7 @@ Some years ago when you wanted to develop a .NET web application it was almost g Suave is a lightweight, non-blocking web server written in F#. It is fairly new to the .NET space, but works wonderfully well and is very idiomatic to functional paradigms. Even though it is still early days it has probably the coolest name and logo amongst its competitors already: -suave, Image by Dustin Moris Gorski +suave, Image by Dustin Moris Gorski After working with Suave for more than a month now I can say that I find it very nice and intuitive. It is a well designed web framework which is easy to work with and allows rapid development if you know how it works. However, if you don't know how it works then you might struggle to get anything done at all. This is exactly what happened to me when I started adopting the framework in the beginning. The documentation is almost non existent and if you look for any advice on how to implement certain things then you are better off by browsing the [GitHub repository](https://github.com/SuaveIO/suave) directly. The lack of documentation is not a mistake, but rather a concious design decision which the Suave team explains as following: diff --git a/src/DustedCodes/BlogPosts/2016_08_22-creating-a-slack-bot-with-fsharp-and-suave-in-less-than-5-minutes.md b/src/DustedCodes/BlogPosts/2016_08_22-creating-a-slack-bot-with-fsharp-and-suave-in-less-than-5-minutes.md index da73fc5..8943997 100644 --- a/src/DustedCodes/BlogPosts/2016_08_22-creating-a-slack-bot-with-fsharp-and-suave-in-less-than-5-minutes.md +++ b/src/DustedCodes/BlogPosts/2016_08_22-creating-a-slack-bot-with-fsharp-and-suave-in-less-than-5-minutes.md @@ -157,15 +157,15 @@ Once deployed I am ready to add a new Slash Commands integration. 2. Pick Slash Commands and then click on the "Add Configuration" button: -slack-slash-commands-add-configuration, Image by Dustin Moris Gorski +slack-slash-commands-add-configuration, Image by Dustin Moris Gorski 3. Choose a command and confirm by clicking on "Add Slash Command Integration": -slack-slash-commands-choose-a-command, Image by Dustin Moris Gorski +slack-slash-commands-choose-a-command, Image by Dustin Moris Gorski 4. Finally type in the URL to your public endpoint and make sure the method is set to POST: -slack-slash-commands-integration-settings, Image by Dustin Moris Gorski +slack-slash-commands-integration-settings, Image by Dustin Moris Gorski 5. Optionally you can set a name, an icon and additional meta data for the bot and then click on the "Save Integration" button. diff --git a/src/DustedCodes/BlogPosts/2016_09_27-load-testing-a-docker-application-with-jmeter-and-amazon-ec2.md b/src/DustedCodes/BlogPosts/2016_09_27-load-testing-a-docker-application-with-jmeter-and-amazon-ec2.md index 23a06b3..6bb8052 100644 --- a/src/DustedCodes/BlogPosts/2016_09_27-load-testing-a-docker-application-with-jmeter-and-amazon-ec2.md +++ b/src/DustedCodes/BlogPosts/2016_09_27-load-testing-a-docker-application-with-jmeter-and-amazon-ec2.md @@ -31,7 +31,7 @@ sudo docker run -p 8080:8888 dustinmoris/docker-demo-nancy:0.2.0 At the end of the script I added a `docker run` command to auto start the container which runs my application under test. Replace this with your own container when launching the instance. -aws-launch-ec2-advanced-details, Image by Dustin Moris Gorski +aws-launch-ec2-advanced-details, Image by Dustin Moris Gorski Simply click through the rest of the wizard and a few minutes later you should be having a running Ubuntu VM with Docker and your application container running inside it. @@ -86,11 +86,11 @@ Once completed you can use the new key file with the PuTTY SSH client to remote 7. Type in a memorable name into the "Saved Sessions" field and click "Save" 8. Finally click on the "Open" button and connect to the VM -putty-save-session, Image by Dustin Moris Gorski +putty-save-session, Image by Dustin Moris Gorski At this point you should be presented with a terminal window and being connected to the JMeter EC2 instance. -putty-ssh-terminal, Image by Dustin Moris Gorski +putty-ssh-terminal, Image by Dustin Moris Gorski ### Upload a JMeter test file to the VM diff --git a/src/DustedCodes/BlogPosts/2016_10_19-building-and-shipping-a-dotnet-core-application-with-docker-and-travisci.md b/src/DustedCodes/BlogPosts/2016_10_19-building-and-shipping-a-dotnet-core-application-with-docker-and-travisci.md index 23723eb..89a87c8 100644 --- a/src/DustedCodes/BlogPosts/2016_10_19-building-and-shipping-a-dotnet-core-application-with-docker-and-travisci.md +++ b/src/DustedCodes/BlogPosts/2016_10_19-building-and-shipping-a-dotnet-core-application-with-docker-and-travisci.md @@ -24,7 +24,7 @@ cd NetCoreDemo Inside that folder I can run `dotnet new --type console` to create a new hello world console application: -dotnet-new-console-app, Image by Dustin Moris Gorski +dotnet-new-console-app, Image by Dustin Moris Gorski For a full reference of the `dotnet new` command check out the [official documentation](https://docs.microsoft.com/en-us/dotnet/articles/core/tools/dotnet-new). @@ -32,7 +32,7 @@ If you don't have the .NET Core CLI available you need to install the [.NET Core After the command has completed I can run `dotnet restore` to restore all dependencies followed by a `dotnet run` which will build and subsequently start the hello world application: -dotnet-restore-and-run, Image by Dustin Moris Gorski +dotnet-restore-and-run, Image by Dustin Moris Gorski This is literally all I had to do to get a simple C# console app running and therefore will stop at this point and move on to the next part where I will set up a build and deployment pipeline in TravisCI. @@ -90,7 +90,7 @@ Here I am essentially calling a second script called `deploy.sh` and passing in The `TRAVIS_TAG` variable is a [default environment variable](https://docs.travis-ci.com/user/environment-variables/#Default-Environment-Variables) which gets set by TravisCI for every build which has been triggered by a tag push and will contain the string value of the tag. `DOCKER_USERNAME` and `DOCKER_PASSWORD` are two custom [environment variables which I have set through the UI](https://docs.travis-ci.com/user/environment-variables/#Defining-Variables-in-Repository-Settings) to follow TravisCI's recommendation to keep sensitive data secret: -travisci-settings-page, Image by Dustin Moris Gorski +travisci-settings-page, Image by Dustin Moris Gorski Another option would have been to [encrypt environment variables](https://docs.travis-ci.com/user/environment-variables/#Encrypting-environment-variables) in the `.travis.yml` file to keep those values secret. Both options are valid as far as I know and it is up to you which one you prefer. @@ -98,7 +98,7 @@ Another option would have been to [encrypt environment variables](https://docs.t If you have to store access credentials to 3rd party platforms like a private registry or the official Docker Hub inside TravisCI then it is highly recommended to register a dedicated user for TravisCI and add that user as an additional collaborator to your Docker Hub repository, so that you can easily limit or revoke access when required: -docker-hub-collaborators, Image by Dustin Moris Gorski +docker-hub-collaborators, Image by Dustin Moris Gorski After defining the `script` and `deploy` step I am basically done with the `.travis.yml` file. diff --git a/src/DustedCodes/BlogPosts/2016_12_15-running-suave-in-aspnet-core-and-on-top-of-kestrel.md b/src/DustedCodes/BlogPosts/2016_12_15-running-suave-in-aspnet-core-and-on-top-of-kestrel.md index 8629e3d..66a5533 100644 --- a/src/DustedCodes/BlogPosts/2016_12_15-running-suave-in-aspnet-core-and-on-top-of-kestrel.md +++ b/src/DustedCodes/BlogPosts/2016_12_15-running-suave-in-aspnet-core-and-on-top-of-kestrel.md @@ -6,7 +6,7 @@ Ho ho ho, happy F# Advent my friends! This is my blog post for the [F# Advent Calendar in English 2016](https://sergeytihon.wordpress.com/2016/10/23/f-advent-calendar-in-english-2016/). First a quick thanks to [Yan Cui](https://twitter.com/theburningmonk) who has pointed out this calendar to me last year and a big thanks to [Sergey Tihon](https://twitter.com/sergey_tihon) who is organising this blogging event and was kind enough to reserve me a spot this year. -santa-suave, Image by Dustin Moris Gorski +santa-suave, Image by Dustin Moris Gorski In this blog post I wanted to write about two technologies which I am particularly excited about: [Suave](https://suave.io/) and [ASP.NET Core](https://www.asp.net/core). Both are frameworks for building web applications, both are written in .NET and both are open source and yet they are very different. [Suave is a lightweight web server](https://github.com/SuaveIO/suave) written entirely in F# and belongs to the family of micro frameworks similar to [NancyFx](http://nancyfx.org/). [ASP.NET Core](https://github.com/aspnet/Home) is Microsoft's new cloud optimised web framework which has been built from the ground up on top of [.NET Core](https://www.microsoft.com/net/core) and all of its goodness. Both are fairly new cutting edge technologies and both are extremely fun to work with. @@ -43,7 +43,7 @@ Even in this simple example you can clearly see the core concept behind Suave. A In theory the one thing required to plug a Suave web app into ASP.NET Core would be to take an incoming HTTP request from ASP.NET Core and convert it into an `HttpContext` in Suave, execute the top level web part, and then translate the resulting `HttpContext` back into an ASP.NET Core response: -suave-in-aspnetcore-concept, Image by Dustin Moris Gorski +suave-in-aspnetcore-concept, Image by Dustin Moris Gorski The other thing which you get with Suave is a self hosted web server which is built into the framework and the traditional way of starting a Suave web application. The `startWebServer` function takes a `SuaveConfig` object and the top level `WebPart` as input parameters. The config object allows web server specific configuration such as HTTP bindings, request quotas, timeout limits and many more things to be set. @@ -115,11 +115,11 @@ Finally I go to the `Startup` class and hook up the Suave `catchAll` web app int Save all, `dotnet restore`, `dotnet build` and `dotnet run`: -running-a-suave-aspnetcore-app, Image by Dustin Moris Gorski +running-a-suave-aspnetcore-app, Image by Dustin Moris Gorski If everything is correct then going to `http://localhost:5000/` should return a successful response like this: -suave-in-aspnetcore-simple-get-request-result, Image by Dustin Moris Gorski +suave-in-aspnetcore-simple-get-request-result, Image by Dustin Moris Gorski You can [check out the sample app in GitHub](https://github.com/dustinmoris/Suave.AspNetCore/tree/master/test/Suave.AspNetCore.App) and try it yourself! diff --git a/src/DustedCodes/BlogPosts/2017_01_19-error-handling-in-aspnet-core.md b/src/DustedCodes/BlogPosts/2017_01_19-error-handling-in-aspnet-core.md index cbcc8d2..e7263f6 100644 --- a/src/DustedCodes/BlogPosts/2017_01_19-error-handling-in-aspnet-core.md +++ b/src/DustedCodes/BlogPosts/2017_01_19-error-handling-in-aspnet-core.md @@ -90,7 +90,7 @@ The `RequestDelegate` is, as its name suggest, a delegate which represents the n This is why the order of the `app.Use...()` method calls matter. When you think about it the underlying pattern can be seen a little bit like an onion: -aspnet-core-middleware-onion-architecture, Image by Dustin Moris Gorski +aspnet-core-middleware-onion-architecture, Image by Dustin Moris Gorski A HTTP request will travel from the top level middleware down to the last middleware, unless a middleware in between can satisfy the request and return a HTTP response earlier to the client. In contrast an unhandled exception would travel from the bottom up. Beginning at the middleware where it got thrown it would bubble up all the way to the top most middleware waiting for something to catch it. diff --git a/src/DustedCodes/BlogPosts/2017_01_24-thank-you-microsoft-for-being-awesome.md b/src/DustedCodes/BlogPosts/2017_01_24-thank-you-microsoft-for-being-awesome.md index 15ba873..bdfbfe9 100644 --- a/src/DustedCodes/BlogPosts/2017_01_24-thank-you-microsoft-for-being-awesome.md +++ b/src/DustedCodes/BlogPosts/2017_01_24-thank-you-microsoft-for-being-awesome.md @@ -6,19 +6,19 @@ OK so I have to admit that I can have a pretty big mouth sometimes, which is certainly not a good quality to have. I don't shy away to be (overly) critical if things annoy me: -tweet-0, Image by Dustin Moris Gorski +tweet-0, Image by Dustin Moris Gorski As you can see, about a year ago I was a little bit upset that a great new product received a not so great new name. I really hoped that [ASP.NET Core](https://www.asp.net/core) would have been named something much "cooler" for my taste. So my frustration continued... -tweet-1, Image by Dustin Moris Gorski +tweet-1, Image by Dustin Moris Gorski *(Let's be honest, it is probably a good thing that I don't have many Twitter followers.)* And even a year later I feel like I am still suffering sometimes: -tweet-2, Image by Dustin Moris Gorski +tweet-2, Image by Dustin Moris Gorski But I don't care about the name anymore. @@ -34,7 +34,7 @@ But I think as someone who depends a lot on these third parties, corporations li Particularly with companies like Microsoft many people feel entitled to be overly rude or critical if things don't work out immediately the way they want it to be (and not just Microsoft but really any company of similar size). It is so easy to forget good manners when one is thinking of a big company logo rather than actual human beings, because it is much easier to throw dirt at a logo than a person. -Microsoft_logo, Image by Dustin Moris Gorski +Microsoft_logo, Image by Dustin Moris Gorski But behind these logos there is still human beings working hard on the products which we use every day and they are not any different from us. Women and men who got into software development and IT for the same good reasons as we did, who share the same passion as we do and who make the same mistakes as us, even when they have only the best in their intention. @@ -69,7 +69,7 @@ This is a lot of (radical) positive change in a comparatively short amount of ti For example, check out the [Surface Studio introduction video](https://www.youtube.com/watch?v=BzMLA8YIgG0) which demonstrates 3D drawing on a Surface Studio: -surface-studio-3d-drawing, Image by Dustin Moris Gorski +surface-studio-3d-drawing, Image by Dustin Moris Gorski You have to admit that this is awesome. I mean I am not even a designer and I get a watery mouth by looking at it. diff --git a/src/DustedCodes/BlogPosts/2017_02_07-functional-aspnet-core.md b/src/DustedCodes/BlogPosts/2017_02_07-functional-aspnet-core.md index a5b6abf..a10a936 100644 --- a/src/DustedCodes/BlogPosts/2017_02_07-functional-aspnet-core.md +++ b/src/DustedCodes/BlogPosts/2017_02_07-functional-aspnet-core.md @@ -8,7 +8,7 @@ In December 2016 I participated in the [F# Advent Calendar](https://sergeytihon. So far this has been pretty good and as of last week the [GitHub repository](https://github.com/SuaveIO/Suave.AspNetCore) has been moved to the official [SuaveIO GitHub organisation](https://github.com/SuaveIO) as well. A while ago someone even tweeted me that the performance has been pretty good too: -suave-aspnet-core-perf-tweet, Image by Dustin Moris Gorski +suave-aspnet-core-perf-tweet, Image by Dustin Moris Gorski Even though this made me very happy there was still one thing that bugged me until today. @@ -175,7 +175,7 @@ With the `bind` function we can combine unlimited `HttpHandler` functions to one The flow would look something like this: -aspnet-core-lambda-http-handler-flow-cropped, Image by Dustin Moris Gorski +aspnet-core-lambda-http-handler-flow-cropped, Image by Dustin Moris Gorski Another very useful combinator which can be borrowed from Suave is the `choose` function. The `choose` function let's you define a list of multiple `HttpHandler` functions which will be iterated one by one until the first `HttpHandler` returns `Some HttpHandlerContext`: diff --git a/src/DustedCodes/BlogPosts/2017_05_14-functional-aspnet-core-part-2-hello-world-from-giraffe.md b/src/DustedCodes/BlogPosts/2017_05_14-functional-aspnet-core-part-2-hello-world-from-giraffe.md index 4643aaf..7247939 100644 --- a/src/DustedCodes/BlogPosts/2017_05_14-functional-aspnet-core-part-2-hello-world-from-giraffe.md +++ b/src/DustedCodes/BlogPosts/2017_05_14-functional-aspnet-core-part-2-hello-world-from-giraffe.md @@ -373,39 +373,39 @@ This basically brings me to the end of this follow up article and I thought what It was a very long day, which started off with a civil ceremony in the morning... -Civil ceremony in the morning of the day, Image by Dustin Moris Gorski +Civil ceremony in the morning of the day, Image by Dustin Moris Gorski Followed by a traditional Hindu ceremony shortly after lunch... -Drums during groom entrance at Hindu ceremony, Image by Dustin Moris Gorski +Drums during groom entrance at Hindu ceremony, Image by Dustin Moris Gorski -Groom entrance at Hindu ceremony, Image by Dustin Moris Gorski +Groom entrance at Hindu ceremony, Image by Dustin Moris Gorski -Prayers at Hindu ceremony, Image by Dustin Moris Gorski +Prayers at Hindu ceremony, Image by Dustin Moris Gorski -Listening to Hindu priest cracking jokes, Image by Dustin Moris Gorski +Listening to Hindu priest cracking jokes, Image by Dustin Moris Gorski I had no idea how much fun Hindu ceremonies can be! There's lots of really fun and merry traditions which take place as part of us getting married. Then there's also a bit of banter between the two families. One of those little traditions is that the bride's family has to steal the groom's shoes before the ceremony ends so that the groom can't leave the house and take his newly wedded wife away from her family - at least not without having to pay for getting his shoes back. Normally this results in a bit of shoe pulling between the bride's side and the groomsmen, but I think in our case it is fair to say that there was a bit of a cultural clash when someone from my family rugby tackled a guy who tried to sneak away with my shoes, lol... -Shoe fight, Image by Dustin Moris Gorski +Shoe fight, Image by Dustin Moris Gorski Luckily nothing serious happened and after everyone had a great laugh we continued with the ceremony... Until finally we were able to celebrate at the reception party in the evening... -Entering at the reception party, Image by Dustin Moris Gorski +Entering at the reception party, Image by Dustin Moris Gorski -Cake cutting, Image by Dustin Moris Gorski +Cake cutting, Image by Dustin Moris Gorski -Our first toast as a married couple, Image by Dustin Moris Gorski +Our first toast as a married couple, Image by Dustin Moris Gorski Throughout the day our guests wrote us lovely (I think) messages on little papers and my family decided to throw all these messages into a wooden box with a nice bottle of Red, which we had to seal ourselves with nails and hammer. We are not allowed to open this box until in seven years time and then we can enjoy a nicely matured bottle of vino while reading all those wonderful memories from our big day. What a brilliant idea! -Sealing box of memories, Image by Dustin Moris Gorski +Sealing box of memories, Image by Dustin Moris Gorski We had a fantastic day and before everyone stormed to the dance floor there was even a pretty impressive surprise firework display... -Surprise firework display, Image by Dustin Moris Gorski +Surprise firework display, Image by Dustin Moris Gorski Getting married was a lot of fun and it all worked out so much better than we could have hoped for :) diff --git a/src/DustedCodes/BlogPosts/2018_02_09-announcing-giraffe-100.md b/src/DustedCodes/BlogPosts/2018_02_09-announcing-giraffe-100.md index e4637a2..ac221bb 100644 --- a/src/DustedCodes/BlogPosts/2018_02_09-announcing-giraffe-100.md +++ b/src/DustedCodes/BlogPosts/2018_02_09-announcing-giraffe-100.md @@ -26,7 +26,7 @@ A much desired and important improvement was the ability to change the default i For the first time Giraffe has detailed XML documentation for all public facing functions available: -giraffe-xml-docs, Image by Dustin Moris Gorski +giraffe-xml-docs, Image by Dustin Moris Gorski Even though this is not a feature itself, it aims at improving the general development experience by providing better IntelliSense and more detailed information when working with Giraffe. diff --git a/src/DustedCodes/BlogPosts/2018_10_08-open-source-documentation.md b/src/DustedCodes/BlogPosts/2018_10_08-open-source-documentation.md index 93de673..6f4e4c2 100644 --- a/src/DustedCodes/BlogPosts/2018_10_08-open-source-documentation.md +++ b/src/DustedCodes/BlogPosts/2018_10_08-open-source-documentation.md @@ -47,13 +47,13 @@ I once had to read the documentation of a third party software which had so many I'm sure that this issue has been fixed by now and I'm not saying that all third party tools have such a bad user experience, but regardless of what their actual UX is, each of them has a slightly different approach on how they structure their content. This is all good in the context of normal (commercial) websites, but it often forgets that documentation is much simpler than the usual website on the internet. Documentation doesn't require half of the things which a normal website can do today. Documentation is a read only exercise. Most importantly, documentation already has an ancient universally understood structure which every human is very likely to be familiar (and comfortable) with: The table of contents. -Table of contents inside a book, Image by Dustin Moris Gorski +Table of contents inside a book, Image by Dustin Moris Gorski A table of contents is so simple and effective that it is still used across all various industries for any content which happens to be larger than a single page. Magazines, books, catalogues, contracts and manuals of all sorts of kind use a table of contents in order to structure their content in a user friendly way. A table of contents let's one structure a large document into smaller pieces without having to divide the content into multiple pages. If it works for print, e-books or large PDFs, then I don't see why it wouldn't work for a project's `DOCUMENTATION.md` file which is hosted on the web: -Table of contents for open source project, Image by Dustin Moris Gorski +Table of contents for open source project, Image by Dustin Moris Gorski Instead of having to break up an online documentation into multiple pages which need to be maintained individually by a person or team, a table of contents allows one to have a single large `DOCUMENTATION.md` file which can be maintained a lot easier without losing the convenience of a structured document. diff --git a/src/DustedCodes/BlogPosts/2020_06_29-github-actions-for-dotnet-core-nuget-packages.md b/src/DustedCodes/BlogPosts/2020_06_29-github-actions-for-dotnet-core-nuget-packages.md index c399de9..77df389 100644 --- a/src/DustedCodes/BlogPosts/2020_06_29-github-actions-for-dotnet-core-nuget-packages.md +++ b/src/DustedCodes/BlogPosts/2020_06_29-github-actions-for-dotnet-core-nuget-packages.md @@ -4,7 +4,7 @@ # GitHub Actions for .NET Core NuGet packages -Last weekend I migrated the [Giraffe web framework](https://github.com/giraffe-fsharp/Giraffe) from [AppVeyor](https://www.appveyor.com) to [GitHub Actions](https://github.com/features/actions). It proved to be incredibly easy to do so despite me having some very specific requirements on how I wanted the final solution to work and that it should be flexible enough to apply to all my other projects too. Even though it was mostly a very straight forward job, there were a few things which I learned along the way which I thought would be worth sharing! +Last weekend I migrated the [Giraffe web framework](https://github.com/giraffe-fsharp/Giraffe) from [AppVeyor](https://www.appveyor.com) to [GitHub Actions](https://github.com/features/actions). It proved to be incredibly easy to do so despite me having some very specific requirements on how I wanted the final solution to work and that it should be flexible enough to apply to all my other projects too. Even though it was mostly a very straight forward job, there were a few things which I learned along the way which I thought would be worth sharing! Here's a quick summary of what I did, why I did it and most importantly how you can apply the same GitHub workflow to your own .NET Core NuGet project as well! @@ -35,9 +35,9 @@ On this premise I decided that each commit, regardless if it happened on a `feat In GitHub, under **Settings** and then **Branches**, one can set up [branch protection rules](https://help.github.com/en/github/administering-a-repository/configuring-protected-branches) for a repository: -[![GitHub Branch Protection Rules](https://storage.googleapis.com/dusted-codes/images/blog-posts/2020-06-28/github-branch-protection-rules.png)](https://storage.googleapis.com/dusted-codes/images/blog-posts/2020-06-28/github-branch-protection-rules.png) +[![GitHub Branch Protection Rules](https://cdn.dusted.codes/images/blog-posts/2020-06-28/github-branch-protection-rules.png)](https://cdn.dusted.codes/images/blog-posts/2020-06-28/github-branch-protection-rules.png) -*Note that the available CI options get automatically updated whenever a CI pipeline is executed and therefore might not show up before the first workflow run has completed.* +*Note that the available CI options get automatically updated whenever a CI pipeline is executed and therefore might not show up before the first workflow run has completed.* We can configure a GitHub Action to trigger builds for commits and pull requests on all branches by providing the `push` and `pull_request` option and leaving the branch definitions blank: @@ -45,7 +45,7 @@ We can configure a GitHub Action to trigger builds for commits and pull requests on: push: pull_request: -``` +``` ### Test on Linux, macOS and Windows @@ -60,15 +60,15 @@ jobs: strategy: matrix: os: [ ubuntu-latest, windows-latest, macos-latest ] -``` +``` In this example I've named the "build" job `build`, which is an arbitrary value and can be changed to anything a user wants. ### Create build artifacts -A build artifact is downloadable output which can be created and collected on each CI run. It can be anything from a single file to an entire folder full of binaries. In the case of a .NET Core NuGet library it is a very useful feature to create a super early version of a NuGet package as soon as a build has finished: +A build artifact is downloadable output which can be created and collected on each CI run. It can be anything from a single file to an entire folder full of binaries. In the case of a .NET Core NuGet library it is a very useful feature to create a super early version of a NuGet package as soon as a build has finished: -[![GitHub Action Build Artifacts](https://storage.googleapis.com/dusted-codes/images/blog-posts/2020-06-28/github-build-artifacts.png)](https://storage.googleapis.com/dusted-codes/images/blog-posts/2020-06-28/github-build-artifacts.png) +[![GitHub Action Build Artifacts](https://cdn.dusted.codes/images/blog-posts/2020-06-28/github-build-artifacts.png)](https://cdn.dusted.codes/images/blog-posts/2020-06-28/github-build-artifacts.png) In combination with pull request triggers this is a super handy way of giving OSS contributors and OSS maintainers an easy way of downloading and testing a NuGet package as part of a PR. @@ -95,7 +95,7 @@ jobs: with: name: nupkg path: ./src/${{ env.PROJECT_NAME }}/bin/Release/*.nupkg -``` +``` In the example above I'm using the pre-defined `GITHUB_RUN_ID` environment variable to specify the NuGet package version and a custom defined environment variable called `PROJECT_NAME` to specify which .NET Core project to pack and publish as an artifact. This has the benefit that the same GitHub workflow definition can be used across multiple projects with very minimal initial setup. @@ -103,7 +103,7 @@ One might have also noticed that I used a wildcard definition for the project fi Lastly I had to use the version 2 (`@v2`) of the `upload-artifact` action in order to use wildcard definitions in the artifact's `path` specification. If you run into a "missing file" error when trying to upload an artifact then make sure that you're using the latest version of this action. Before version 2 wildcards were not supported yet. -On another note, the `if: matrix.os == 'ubuntu-latest'` condition as part of the `Pack` and `Upload Artifact` steps has no special purpose except limiting the artifact upload to a single run from the previously defined build matrix. A single artifact upload is sufficient (the NuGet package doesn't change based on the environment where it has been packed) and therefore I simply chose `ubuntu-latest` because Ubuntu happens to be the fastest executing environment and therefore helps to keep the overall build time as low as possible. Windows workers seem to take generally longer to start than macOS or Ubuntu. +On another note, the `if: matrix.os == 'ubuntu-latest'` condition as part of the `Pack` and `Upload Artifact` steps has no special purpose except limiting the artifact upload to a single run from the previously defined build matrix. A single artifact upload is sufficient (the NuGet package doesn't change based on the environment where it has been packed) and therefore I simply chose `ubuntu-latest` because Ubuntu happens to be the fastest executing environment and therefore helps to keep the overall build time as low as possible. Windows workers seem to take generally longer to start than macOS or Ubuntu. ### Push nightly releases to GitHub packages @@ -181,11 +181,11 @@ Now one might wonder why I used a `curl` command to interact with GitHub's HTTP If everything went to plan then the NuGet packages will get uploaded to the user's or organisation's own GitHub packages feed: -[![GitHub Packages Feed](https://storage.googleapis.com/dusted-codes/images/blog-posts/2020-06-28/github-packages-feed.png)](https://storage.googleapis.com/dusted-codes/images/blog-posts/2020-06-28/github-packages-feed.png) +[![GitHub Packages Feed](https://cdn.dusted.codes/images/blog-posts/2020-06-28/github-packages-feed.png)](https://cdn.dusted.codes/images/blog-posts/2020-06-28/github-packages-feed.png) The packages are tagged with the `GITHUB_RUN_ID` (unless it was a GitHub release): -[![GitHub package versions](https://storage.googleapis.com/dusted-codes/images/blog-posts/2020-06-28/github-package-versions.png)](https://storage.googleapis.com/dusted-codes/images/blog-posts/2020-06-28/github-package-versions.png) +[![GitHub package versions](https://cdn.dusted.codes/images/blog-posts/2020-06-28/github-package-versions.png)](https://cdn.dusted.codes/images/blog-posts/2020-06-28/github-package-versions.png) This is by design. It makes it very easy to associate a certain package version to a specific nightly run. It also makes it very obvious that a package version is a nightly build and not an official release and it's easy to know when a newer version is available since the `GITHUB_RUN_ID` is an incremental counter. @@ -193,7 +193,7 @@ This is by design. It makes it very easy to associate a certain package version GitHub has a wonderful [concept of releases](https://help.github.com/en/github/administering-a-repository/about-releases), which is an extra layer on top of git tags and which provide a nice UI to create, manage and view a release: -[![GitHub Release](https://storage.googleapis.com/dusted-codes/images/blog-posts/2020-06-28/github-view-release.png)](https://storage.googleapis.com/dusted-codes/images/blog-posts/2020-06-28/github-view-release.png) +[![GitHub Release](https://cdn.dusted.codes/images/blog-posts/2020-06-28/github-view-release.png)](https://cdn.dusted.codes/images/blog-posts/2020-06-28/github-view-release.png) Personally I like to use GitHub releases as a formal and conscious step to create, document and publish a NuGet package. For that reason I've added the `release` option as an additional CI trigger: @@ -277,7 +277,7 @@ arrTag[1]: tags arrTag[2]: v1.2.3 ``` -The next couple lines grab the version tag from the third array element (second index) and later strip the leading `v` character from the value: +The next couple lines grab the version tag from the third array element (second index) and later strip the leading `v` character from the value: ``` VERSION="${arrTag[2]}" @@ -292,7 +292,7 @@ In the end the `VERSION` variable holds the correct release version of the immin dotnet pack -c Release --include-symbols --include-source -p:PackageVersion=$VERSION -o nupkg src/$PROJECT_NAME/$PROJECT_NAME.*proj ``` -This is a very effective way of correctly versioning NuGet releases and keeping them automatically synced with GitHub releases (or manual git tags). It also enforces that a release can only happen when a proper git tag has been created. +This is a very effective way of correctly versioning NuGet releases and keeping them automatically synced with GitHub releases (or manual git tags). It also enforces that a release can only happen when a proper git tag has been created. ### Speed @@ -341,7 +341,7 @@ Running `bash` scripts is significantly faster than running PowerShell (`pwsh`), Overall these micro improvements mean that an incoming pull request takes approximately two minutes to successfully build against the entire build matrix and produce a NuGet artifact as well: -[![GitHub Action Build time for Giraffe](https://storage.googleapis.com/dusted-codes/images/blog-posts/2020-06-28/github-action-run-time.png)](https://storage.googleapis.com/dusted-codes/images/blog-posts/2020-06-28/github-action-run-time.png) +[![GitHub Action Build time for Giraffe](https://cdn.dusted.codes/images/blog-posts/2020-06-28/github-action-run-time.png)](https://cdn.dusted.codes/images/blog-posts/2020-06-28/github-action-run-time.png) ## Environment Variables @@ -385,7 +385,7 @@ The `GITHUB_FEED` variable is a convenience pointer to the user's or organisatio Finally the `NUGET_FEED` variable points towards the official NuGet gallery and the `NUGET_KEY` variable receives a private secret which must be set up manually either at the project or organisation level of the GitHub repository: -[![GitHub Secrets](https://storage.googleapis.com/dusted-codes/images/blog-posts/2020-06-28/github-secrets.png)](https://storage.googleapis.com/dusted-codes/images/blog-posts/2020-06-28/github-secrets.png) +[![GitHub Secrets](https://cdn.dusted.codes/images/blog-posts/2020-06-28/github-secrets.png)](https://cdn.dusted.codes/images/blog-posts/2020-06-28/github-secrets.png) I configured the `NUGET_KEY` as an organisation wide secret, which means I don't have to set up any additional secrets for each repository any more. @@ -395,7 +395,7 @@ If this rings your security bells then you are not entirely wrong. If you wonder The value for the `NUGET_KEY` secret has to be generated on [www.nuget.org](https://www.nuget.org): -[![NuGet API Key](https://storage.googleapis.com/dusted-codes/images/blog-posts/2020-06-28/nuget-key.png)](https://storage.googleapis.com/dusted-codes/images/blog-posts/2020-06-28/nuget-key.png) +[![NuGet API Key](https://cdn.dusted.codes/images/blog-posts/2020-06-28/nuget-key.png)](https://cdn.dusted.codes/images/blog-posts/2020-06-28/nuget-key.png) ## The End Result diff --git a/src/DustedCodes/BlogPosts/2020_07_22-dotnet-for-beginners.md b/src/DustedCodes/BlogPosts/2020_07_22-dotnet-for-beginners.md index d4b9456..dc3af7e 100644 --- a/src/DustedCodes/BlogPosts/2020_07_22-dotnet-for-beginners.md +++ b/src/DustedCodes/BlogPosts/2020_07_22-dotnet-for-beginners.md @@ -41,7 +41,7 @@ Let me introduce you to the **6 Sins of .NET**: If a person sets out to learn C# (like the author from the question above), what do they learn? Is it C# or .NET? The answer is both. C# doesn't exist without .NET and you cannot program .NET without C# (or F# or VB.NET for that matter). This is not a problem in itself, but certainly where some of the issues begin. A new beginner doesn't just learn C# but also has to learn the inner workings of .NET. Things get even more confusing when C# isn't the initial .NET language to learn. Supposedly the loose relationship between .NET languages shouldn't really matter because they compile into IL and become cross compatible. [Except when they don't](https://github.com/dotnet/vblang/issues/300): -[![C# unmanaged constraint language leak](https://storage.googleapis.com/dusted-codes/images/blog-posts/2020-07-05/csharp-unmanaged-constraint-leak.png)](https://storage.googleapis.com/dusted-codes/images/blog-posts/2020-07-05/csharp-unmanaged-constraint-leak.png) +[![C# unmanaged constraint language leak](https://cdn.dusted.codes/images/blog-posts/2020-07-05/csharp-unmanaged-constraint-leak.png)](https://cdn.dusted.codes/images/blog-posts/2020-07-05/csharp-unmanaged-constraint-leak.png) The [BCL (Base Class Libraries)](https://docs.microsoft.com/en-us/dotnet/standard/framework-libraries#base-class-libraries) provide the foundation for all three languages, yet they are only written in C#. That's not really an issue unless entire features were written with only one language in mind and are extremely cumbersome to use from another. For example, F# still doesn't have [native support for `Task` and `Task`](https://github.com/fsharp/fslang-suggestions/issues/581), converting between `System.Collection.*` classes and F# types is a painful undertaking and F# functions and .NET `Action` objects don't map very well. @@ -51,7 +51,7 @@ Interop issues between those three languages are a big burden on new developers, Take this [StackOverflow question](https://stackoverflow.com/questions/2570814/when-to-use-abstract-classes) for an example. Nothing which has been described in the accepted (and most upvoted) answer below isn't also true for interfaces with default method implementations today: -[![Abstract class question on StackOverflow](https://storage.googleapis.com/dusted-codes/images/blog-posts/2020-07-05/abstract-class-question-stack-overflow.png)](https://storage.googleapis.com/dusted-codes/images/blog-posts/2020-07-05/abstract-class-question-stack-overflow.png) +[![Abstract class question on StackOverflow](https://cdn.dusted.codes/images/blog-posts/2020-07-05/abstract-class-question-stack-overflow.png)](https://cdn.dusted.codes/images/blog-posts/2020-07-05/abstract-class-question-stack-overflow.png) Another great example is the growing variety of C# data types. When is it appropriate to create a [data class](https://docs.microsoft.com/en-us/aspnet/web-api/overview/data/using-web-api-with-entity-framework/part-5), an [immutable data class](https://www.c-sharpcorner.com/article/all-about-c-sharp-immutable-classes2/), a [mutable struct](http://mustoverride.com/tuples_structs/), an [immutable struct](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/struct#readonly-struct), a [tuple class](https://docs.microsoft.com/en-us/dotnet/api/system.tuple?view=netcore-3.1), the new concept of [named tuples](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/value-tuples) or the upcoming [record type](https://www.stevefenton.co.uk/2020/05/csharp-9-record-types/)? @@ -59,31 +59,31 @@ Seasoned .NET developers are very opinionated about when to use each of these ty The irony is that many of the newly added language and framework features are supposed to make .NET easier to learn: -[![Make .NET easier](https://storage.googleapis.com/dusted-codes/images/blog-posts/2020-07-05/tweet-dotnet-easier.png)](https://twitter.com/shanselman/status/1281856685657616384) +[![Make .NET easier](https://cdn.dusted.codes/images/blog-posts/2020-07-05/tweet-dotnet-easier.png)](https://twitter.com/shanselman/status/1281856685657616384) *(BTW, I have been a huge proponent of dropping the `.csproj` and `.sln` files for a very long time but previously Microsoft employees defended them as if someone offended their family, so it's nice to finally see some support for that idea too! :))* Don't get me wrong, I agree with Scott that \*this particular feature\* will make writing a first hello world app a lot easier than before, however, our friend Joseph Woodward makes a good point that nothing comes for free: -[![With great power comes great responsibility](https://storage.googleapis.com/dusted-codes/images/blog-posts/2020-07-05/tweet-dotnet-reboot-3.png)](https://twitter.com/joe_mighty/status/1281899362281566208) +[![With great power comes great responsibility](https://cdn.dusted.codes/images/blog-posts/2020-07-05/tweet-dotnet-reboot-3.png)](https://twitter.com/joe_mighty/status/1281899362281566208) And he's not alone with this idea: -[![.NET is hard](https://storage.googleapis.com/dusted-codes/images/blog-posts/2020-07-05/tweet-dotnet-hard-2.png)](https://twitter.com/buhakmeh/status/1281985930279223298) +[![.NET is hard](https://cdn.dusted.codes/images/blog-posts/2020-07-05/tweet-dotnet-hard-2.png)](https://twitter.com/buhakmeh/status/1281985930279223298) -[![.NET is hard](https://storage.googleapis.com/dusted-codes/images/blog-posts/2020-07-05/tweet-dotnet-hard-3.png)](https://twitter.com/FransBouma/status/1282059062247555082) +[![.NET is hard](https://cdn.dusted.codes/images/blog-posts/2020-07-05/tweet-dotnet-hard-3.png)](https://twitter.com/FransBouma/status/1282059062247555082) It's not just about learning how to write C#. A huge part of learning .NET is reading other people's code, which is also getting inherently more difficult as a result: -[![.NET is hard](https://storage.googleapis.com/dusted-codes/images/blog-posts/2020-07-05/tweet-dotnet-hard-4.png)](https://twitter.com/1amjau/status/1281908190167347200) +[![.NET is hard](https://cdn.dusted.codes/images/blog-posts/2020-07-05/tweet-dotnet-hard-4.png)](https://twitter.com/1amjau/status/1281908190167347200) Whilst I do like and support the idea of adding more features to C#, I cannot ignore the fact that it also takes its toll. Some people raised a good point that it might be time to consider making some old features obsolete: -[![.NET is hard](https://storage.googleapis.com/dusted-codes/images/blog-posts/2020-07-05/tweet-dotnet-reboot-2.png)](https://twitter.com/RustyF/status/1281860368369963008) +[![.NET is hard](https://cdn.dusted.codes/images/blog-posts/2020-07-05/tweet-dotnet-reboot-2.png)](https://twitter.com/RustyF/status/1281860368369963008) -[![.NET is hard](https://storage.googleapis.com/dusted-codes/images/blog-posts/2020-07-05/tweet-dotnet-reboot-1.png)](https://twitter.com/RehanSaeedUK/status/1281943982189228032) +[![.NET is hard](https://cdn.dusted.codes/images/blog-posts/2020-07-05/tweet-dotnet-reboot-1.png)](https://twitter.com/RehanSaeedUK/status/1281943982189228032) Whatever one's personal opinion is, "feature bloat" is certainly becoming a growing concern in the C# community and Microsoft would be stupid not to listen or at least take some notes. @@ -95,13 +95,13 @@ As mentioned above, all three .NET languages evolve independently from .NET. C# Now I understand that all these things are very different and I'm comparing apples with oranges, but how is a new developer supposed to know all of that? All these components are independent and yet correlated enough to overwhelm a new developer with screens like this: -[![.NET Core Versions](https://storage.googleapis.com/dusted-codes/images/blog-posts/2020-07-05/dotnet-core-versions.png)](https://storage.googleapis.com/dusted-codes/images/blog-posts/2020-07-05/dotnet-core-versions.png) +[![.NET Core Versions](https://cdn.dusted.codes/images/blog-posts/2020-07-05/dotnet-core-versions.png)](https://cdn.dusted.codes/images/blog-posts/2020-07-05/dotnet-core-versions.png) Even .NET developers with 5+ years of experience find the above information hard to digest. I know this for a fact because I ask this question in interviews a lot and it's rare to get a correct explanation. The problem is not that this information is too verbose, or wrong, or unnecessary to know, but rather the unfortunate case that it's just how big .NET has become. In fact it's even bigger but I believe that the .NET team has already tried their best to condense this screen into the simplest form possible. I understand the difficulty - if you've got a very mature and feature rich platform then there's a lot to explain - but nevertheless it's not a particularly sexy look. In contrast to .NET this is the cognitive load which is thrown at a beginner in Go: -[![Go Versions](https://storage.googleapis.com/dusted-codes/images/blog-posts/2020-07-05/go-versions.png)](https://storage.googleapis.com/dusted-codes/images/blog-posts/2020-07-05/go-versions.png) +[![Go Versions](https://cdn.dusted.codes/images/blog-posts/2020-07-05/go-versions.png)](https://cdn.dusted.codes/images/blog-posts/2020-07-05/go-versions.png) It's much simpler in every way. Admittedly it's not an entirely fair comparison because Go gets compiled directly into machine code and therefore there isn't a real distinction between SDK and runtime, but my point is still the same. There is certainly plenty of room for improvement and I don't think what we see there today is the best we can do. @@ -111,7 +111,7 @@ Maybe there's some value in officially aligning language, ASP.NET Core and .NET Now this one will probably hit some nerves, but one of the \*big\* problems with .NET is that Microsoft is obsessed with the idea of [.NET Everywhere](https://www.hanselman.com/blog/NETEverywhereApparentlyAlsoMeansWindows311AndDOS.aspx). Every [new development](https://visualstudiomagazine.com/articles/2020/06/30/uno-visual-studio.aspx) aims at [unifying everything](https://devblogs.microsoft.com/dotnet/introducing-net-multi-platform-app-ui/) into [one big platform](https://dotnet.microsoft.com/learn/dotnet/what-is-dotnet), catering for [every](https://dotnet.microsoft.com/apps/xamarin/mobile-apps) [single](https://dotnet.microsoft.com/apps/iot) [possible](https://dotnet.microsoft.com/apps/gaming) [use](https://dotnet.microsoft.com/apps/cloud) [case](https://dotnet.microsoft.com/apps/desktop) [imaginable](https://dotnet.microsoft.com/apps/machinelearning-ai): -[![.NET Everywhere](https://storage.googleapis.com/dusted-codes/images/blog-posts/2020-07-05/dotnet-5-everywhere.png)](https://storage.googleapis.com/dusted-codes/images/blog-posts/2020-07-05/dotnet-5-everywhere.png) +[![.NET Everywhere](https://cdn.dusted.codes/images/blog-posts/2020-07-05/dotnet-5-everywhere.png)](https://cdn.dusted.codes/images/blog-posts/2020-07-05/dotnet-5-everywhere.png) *(Thanks to the courtesy of [Ben Adams](https://twitter.com/ben_a_adams) I've updated the graphic to represent the [full picture of .NET](https://twitter.com/ben_a_adams/status/1286144227819257856). Ben created this image for the purpose of his own blog which you can read on [www.ageofascent.com/blog](https://www.ageofascent.com/blog/).)* @@ -119,7 +119,7 @@ In many ways it makes a lot of sense, but the angle taken is causing more harm t Unifying the entire stack into a single .NET platform doesn't come without a price. For years things have been constantly moved around, new things have been created and others have been dropped. Only recently I had to re-factor my ASP.NET Core startup code yet again: -[![.NET Core 3.1 - Refactoring web host to generic host](https://storage.googleapis.com/dusted-codes/images/blog-posts/2020-07-05/dotnet-generic-web-host-builder.png)](https://storage.googleapis.com/dusted-codes/images/blog-posts/2020-07-05/dotnet-generic-web-host-builder.png) +[![.NET Core 3.1 - Refactoring web host to generic host](https://cdn.dusted.codes/images/blog-posts/2020-07-05/dotnet-generic-web-host-builder.png)](https://cdn.dusted.codes/images/blog-posts/2020-07-05/dotnet-generic-web-host-builder.png) Every attempt at unifying things for unification's sake makes simple code more verbose. Without a doubt the previous code was a lot easier to understand. I applaud the concept of a [generic host](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/generic-host?view=aspnetcore-3.1), but one has to wonder how often does a developer actually want to combine two servers into one? In my opinion there must be a real benefit in order to justify complexity such as having a builder inside another builder! How often does a developer want to \*create\* a web server and not \*run\* it as well? I'm sure these edge cases do exist, but why can't they be hidden from the other 99.9% of use cases which normally take place? @@ -139,13 +139,13 @@ Microsoft's "Swiss Army Knife" approach creates an unnecessary burden For example, here's the output of all the default .NET templates which get shipped as part of the .NET CLI (minus the Giraffe one): -[![.NET Project Templates](https://storage.googleapis.com/dusted-codes/images/blog-posts/2020-07-05/dotnet-new-command.png)](https://storage.googleapis.com/dusted-codes/images/blog-posts/2020-07-05/dotnet-new-command.png) +[![.NET Project Templates](https://cdn.dusted.codes/images/blog-posts/2020-07-05/dotnet-new-command.png)](https://cdn.dusted.codes/images/blog-posts/2020-07-05/dotnet-new-command.png) They barely fit on a single screen. Again, it's great that Microsoft has Blazor as an answer to WASM, or that they have WPF as an option for Windows, but why are these things shipped together as one big ugly thing? Why can't there just be a template for a console app or class library and then some text which explains how to download more? This is a classic example where ".NET Everywhere" is getting into most users' way! Speaking of fitting things into a single screen... -[![Visual Studio Chaos](https://storage.googleapis.com/dusted-codes/images/blog-posts/2020-07-05/visual-studio-chaos.png)](https://twitter.com/dylanbeattie/status/832326857798348800) +[![Visual Studio Chaos](https://cdn.dusted.codes/images/blog-posts/2020-07-05/visual-studio-chaos.png)](https://twitter.com/dylanbeattie/status/832326857798348800) Whilst the above tweet was comical in nature, it's not far from the truth. @@ -165,13 +165,13 @@ Another problem which I have observed is Microsoft's fear of being ignored. Micr Microsoft does not miss a single opportunity to advertise a whole range of products when someone just looks at one. Doing .NET development? Oh look, here is Visual Studio which can help you with that! Oh and by the way you can deploy directly to IIS! In case you wonder, IIS comes with Windows Server which also runs in Azure! Aha, speaking of Azure, did you know that you can also click this button which will automatically create a subscription and deploy to the cloud? In case you don't like right-click-publish we also have Azure DevOps, which is a fully featured CI/CD pipeline! Of course there's no pressure, but if you \*do sign up now\* then we'll give you free credits for a month! Anyway, it's just a "friendly reminder" so you know that in theory we offer the full package in case you need it! C'mon look at me, look at me, look at me now! -[![Look at me - Attention seeker](https://storage.googleapis.com/dusted-codes/images/blog-posts/2020-07-05/look-at-me.gif)](https://storage.googleapis.com/dusted-codes/images/blog-posts/2020-07-05/look-at-me.gif) +[![Look at me - Attention seeker](https://cdn.dusted.codes/images/blog-posts/2020-07-05/look-at-me.gif)](https://cdn.dusted.codes/images/blog-posts/2020-07-05/look-at-me.gif) Again, I totally understand why Microsoft does what they do (and I'm sure there's a lot of good intention there), but it comes across the complete wrong and opposite way. No wonder that the perception of .NET hasn't changed much in the outside world: -[![.NET coming across the wrong way](https://storage.googleapis.com/dusted-codes/images/blog-posts/2020-07-05/dotnet-microsoft-bloatware.png)](https://twitter.com/blazorguy/status/1279092538490736640) +[![.NET coming across the wrong way](https://cdn.dusted.codes/images/blog-posts/2020-07-05/dotnet-microsoft-bloatware.png)](https://twitter.com/blazorguy/status/1279092538490736640) What Microsoft really tries to say is: @@ -185,7 +185,7 @@ On one hand Microsoft wants to create this new brand of an open source, cross pl The other thing is that [questions and answers like these](https://www.reddit.com/r/csharp/comments/htlgsr/vs_or_vs_code_problem/) need to stop: -[![Visual Studio vs. Visual Studio Code](https://storage.googleapis.com/dusted-codes/images/blog-posts/2020-07-05/visual-studio-vs-visual-studio-code-question.png)](https://storage.googleapis.com/dusted-codes/images/blog-posts/2020-07-05/visual-studio-vs-visual-studio-code-question.png) +[![Visual Studio vs. Visual Studio Code](https://cdn.dusted.codes/images/blog-posts/2020-07-05/visual-studio-vs-visual-studio-code-question.png)](https://cdn.dusted.codes/images/blog-posts/2020-07-05/visual-studio-vs-visual-studio-code-question.png) This question and particularly the answer are really bad, because they demonstrate that whilst C# and .NET are not necessarily tied to Visual Studio and Windows anymore, they still remain the most viable option to date. This sentiment is not good but unfortunately true. I know from my own experience that the [Visual Studio Code plugin for C#](https://code.visualstudio.com/Docs/languages/csharp) is nowhere near as good as it should be. The same applies to F#. Why is that? It's not that Visual Studio Code is less capable than Visual Studio, but rather a decision by Microsoft to give it a lower priority and a lack of investment. I don't need to use [JetBrains GoLand](https://www.jetbrains.com/go/) in order to be productive in Go, but I have to use [Rider](https://www.jetbrains.com/rider/) for .NET. @@ -193,7 +193,7 @@ Microsoft needs to decouple .NET from everything else and make it a great standa C#, F# and .NET will always be perceived as a very Microsoft and Windows centric development environment when even the [official .NET documentation](https://docs.microsoft.com/en-us/dotnet/) page confirms a critic's worst fears: -[![.NET Documentation](https://storage.googleapis.com/dusted-codes/images/blog-posts/2020-07-05/official-dotnet-docs.png)](https://storage.googleapis.com/dusted-codes/images/blog-posts/2020-07-05/official-dotnet-docs.png) +[![.NET Documentation](https://cdn.dusted.codes/images/blog-posts/2020-07-05/official-dotnet-docs.png)](https://cdn.dusted.codes/images/blog-posts/2020-07-05/official-dotnet-docs.png) ### Architecture Break Down @@ -227,7 +227,7 @@ At least Microsoft is consistent with its naming. There's something comical in t C#, F# and the whole of .NET is a great development platform to code, but it has also become overly complex which is holding new developers back. I've been working with it for many years and mostly enjoyed myself, however I won't lie and say that things haven't gotten a little bit out of hand lately. There is something to tell that after having .NET for 20 years the programming community still hasn't seen anything new or noteworthy since the creation of [stackoverflow.com](https://stackoverflow.com): -[![Famous .NET website question on Quora](https://storage.googleapis.com/dusted-codes/images/blog-posts/2020-07-05/famous-dotnet-websites.png)](https://storage.googleapis.com/dusted-codes/images/blog-posts/2020-07-05/famous-dotnet-websites.png) +[![Famous .NET website question on Quora](https://cdn.dusted.codes/images/blog-posts/2020-07-05/famous-dotnet-websites.png)](https://cdn.dusted.codes/images/blog-posts/2020-07-05/famous-dotnet-websites.png) Meanwhile we've seen very prominent products being built with other languages spanning across multiple domains such as developer technologies ([Docker](https://www.docker.com), [Kubernetes](https://kubernetes.io), [Prometheus](https://github.com/prometheus)) to smaller static website generators ([Hugo](https://gohugo.io)) or some of the most successful FinTech startups ([Monzo](https://monzo.com)) in the world. diff --git a/src/DustedCodes/CSS/fonts.css b/src/DustedCodes/CSS/fonts.css index 6ccdb46..1bfa30c 100644 --- a/src/DustedCodes/CSS/fonts.css +++ b/src/DustedCodes/CSS/fonts.css @@ -4,7 +4,7 @@ font-style: normal; font-weight: 700; font-display: swap; - src: local('Martel Bold'), local('Martel-Bold'), url(https://storage.googleapis.com/dusted-codes/fonts/Martel/Martel-Bold-devanagari.woff2) format('woff2'); + src: local('Martel Bold'), local('Martel-Bold'), url(https://cdn.dusted.codes/fonts/Martel/Martel-Bold-devanagari.woff2) format('woff2'); unicode-range: U+0900-097F, U+1CD0-1CF6, U+1CF8-1CF9, U+200C-200D, U+20A8, U+20B9, U+25CC, U+A830-A839, U+A8E0-A8FB; } /* latin-ext */ @@ -13,7 +13,7 @@ font-style: normal; font-weight: 700; font-display: swap; - src: local('Martel Bold'), local('Martel-Bold'), url(https://storage.googleapis.com/dusted-codes/fonts/Martel/Martel-Bold-latin-ext.woff2) format('woff2'); + src: local('Martel Bold'), local('Martel-Bold'), url(https://cdn.dusted.codes/fonts/Martel/Martel-Bold-latin-ext.woff2) format('woff2'); unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; } /* latin */ @@ -22,7 +22,7 @@ font-style: normal; font-weight: 700; font-display: swap; - src: local('Martel Bold'), local('Martel-Bold'), url(https://storage.googleapis.com/dusted-codes/fonts/Martel/Martel-Bold-latin.woff2) format('woff2'); + src: local('Martel Bold'), local('Martel-Bold'), url(https://cdn.dusted.codes/fonts/Martel/Martel-Bold-latin.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } /* devanagari */ @@ -31,7 +31,7 @@ font-style: normal; font-weight: 800; font-display: swap; - src: local('Martel ExtraBold'), local('Martel-ExtraBold'), url(https://storage.googleapis.com/dusted-codes/fonts/Martel/Martel-ExtraBold-devanagari.woff2) format('woff2'); + src: local('Martel ExtraBold'), local('Martel-ExtraBold'), url(https://cdn.dusted.codes/fonts/Martel/Martel-ExtraBold-devanagari.woff2) format('woff2'); unicode-range: U+0900-097F, U+1CD0-1CF6, U+1CF8-1CF9, U+200C-200D, U+20A8, U+20B9, U+25CC, U+A830-A839, U+A8E0-A8FB; } /* latin-ext */ @@ -40,7 +40,7 @@ font-style: normal; font-weight: 800; font-display: swap; - src: local('Martel ExtraBold'), local('Martel-ExtraBold'), url(https://storage.googleapis.com/dusted-codes/fonts/Martel/Martel-ExtraBold-latin-ext.woff2) format('woff2'); + src: local('Martel ExtraBold'), local('Martel-ExtraBold'), url(https://cdn.dusted.codes/fonts/Martel/Martel-ExtraBold-latin-ext.woff2) format('woff2'); unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; } /* latin */ @@ -49,7 +49,7 @@ font-style: normal; font-weight: 800; font-display: swap; - src: local('Martel ExtraBold'), local('Martel-ExtraBold'), url(https://storage.googleapis.com/dusted-codes/fonts/Martel/Martel-ExtraBold-latin.woff2) format('woff2'); + src: local('Martel ExtraBold'), local('Martel-ExtraBold'), url(https://cdn.dusted.codes/fonts/Martel/Martel-ExtraBold-latin.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } /* cyrillic-ext */ @@ -58,7 +58,7 @@ font-style: italic; font-weight: 600; font-display: swap; - src: local('Nunito SemiBold Italic'), local('Nunito-SemiBoldItalic'), url(https://storage.googleapis.com/dusted-codes/fonts/Nunito/Nunito-SemiBoldItalic-cyrillic-ext.woff2) format('woff2'); + src: local('Nunito SemiBold Italic'), local('Nunito-SemiBoldItalic'), url(https://cdn.dusted.codes/fonts/Nunito/Nunito-SemiBoldItalic-cyrillic-ext.woff2) format('woff2'); unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; } /* cyrillic */ @@ -67,7 +67,7 @@ font-style: italic; font-weight: 600; font-display: swap; - src: local('Nunito SemiBold Italic'), local('Nunito-SemiBoldItalic'), url(https://storage.googleapis.com/dusted-codes/fonts/Nunito/Nunito-SemiBoldItalic-cyrillic.woff2) format('woff2'); + src: local('Nunito SemiBold Italic'), local('Nunito-SemiBoldItalic'), url(https://cdn.dusted.codes/fonts/Nunito/Nunito-SemiBoldItalic-cyrillic.woff2) format('woff2'); unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; } /* vietnamese */ @@ -76,7 +76,7 @@ font-style: italic; font-weight: 600; font-display: swap; - src: local('Nunito SemiBold Italic'), local('Nunito-SemiBoldItalic'), url(https://storage.googleapis.com/dusted-codes/fonts/Nunito/Nunito-SemiBoldItalic-vietnamese.woff2) format('woff2'); + src: local('Nunito SemiBold Italic'), local('Nunito-SemiBoldItalic'), url(https://cdn.dusted.codes/fonts/Nunito/Nunito-SemiBoldItalic-vietnamese.woff2) format('woff2'); unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB; } /* latin-ext */ @@ -85,7 +85,7 @@ font-style: italic; font-weight: 600; font-display: swap; - src: local('Nunito SemiBold Italic'), local('Nunito-SemiBoldItalic'), url(https://storage.googleapis.com/dusted-codes/fonts/Nunito/Nunito-SemiBoldItalic-latin-ext.woff2) format('woff2'); + src: local('Nunito SemiBold Italic'), local('Nunito-SemiBoldItalic'), url(https://cdn.dusted.codes/fonts/Nunito/Nunito-SemiBoldItalic-latin-ext.woff2) format('woff2'); unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; } /* latin */ @@ -94,7 +94,7 @@ font-style: italic; font-weight: 600; font-display: swap; - src: local('Nunito SemiBold Italic'), local('Nunito-SemiBoldItalic'), url(https://storage.googleapis.com/dusted-codes/fonts/Nunito/Nunito-SemiBoldItalic-latin.woff2) format('woff2'); + src: local('Nunito SemiBold Italic'), local('Nunito-SemiBoldItalic'), url(https://cdn.dusted.codes/fonts/Nunito/Nunito-SemiBoldItalic-latin.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } /* cyrillic-ext */ @@ -103,7 +103,7 @@ font-style: normal; font-weight: 300; font-display: swap; - src: local('Nunito Light'), local('Nunito-Light'), url(https://storage.googleapis.com/dusted-codes/fonts/Nunito/Nunito-Light-cyrillic-ext.woff2) format('woff2'); + src: local('Nunito Light'), local('Nunito-Light'), url(https://cdn.dusted.codes/fonts/Nunito/Nunito-Light-cyrillic-ext.woff2) format('woff2'); unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; } /* cyrillic */ @@ -112,7 +112,7 @@ font-style: normal; font-weight: 300; font-display: swap; - src: local('Nunito Light'), local('Nunito-Light'), url(https://storage.googleapis.com/dusted-codes/fonts/Nunito/Nunito-Light-cyrillic.woff2) format('woff2'); + src: local('Nunito Light'), local('Nunito-Light'), url(https://cdn.dusted.codes/fonts/Nunito/Nunito-Light-cyrillic.woff2) format('woff2'); unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; } /* vietnamese */ @@ -121,7 +121,7 @@ font-style: normal; font-weight: 300; font-display: swap; - src: local('Nunito Light'), local('Nunito-Light'), url(https://storage.googleapis.com/dusted-codes/fonts/Nunito/Nunito-Light-vietnamese.woff2) format('woff2'); + src: local('Nunito Light'), local('Nunito-Light'), url(https://cdn.dusted.codes/fonts/Nunito/Nunito-Light-vietnamese.woff2) format('woff2'); unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB; } /* latin-ext */ @@ -130,7 +130,7 @@ font-style: normal; font-weight: 300; font-display: swap; - src: local('Nunito Light'), local('Nunito-Light'), url(https://storage.googleapis.com/dusted-codes/fonts/Nunito/Nunito-Light-latin-ext.woff2) format('woff2'); + src: local('Nunito Light'), local('Nunito-Light'), url(https://cdn.dusted.codes/fonts/Nunito/Nunito-Light-latin-ext.woff2) format('woff2'); unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; } /* latin */ @@ -139,7 +139,7 @@ font-style: normal; font-weight: 300; font-display: swap; - src: local('Nunito Light'), local('Nunito-Light'), url(https://storage.googleapis.com/dusted-codes/fonts/Nunito/Nunito-Light-latin.woff2) format('woff2'); + src: local('Nunito Light'), local('Nunito-Light'), url(https://cdn.dusted.codes/fonts/Nunito/Nunito-Light-latin.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } /* cyrillic-ext */ @@ -148,7 +148,7 @@ font-style: normal; font-weight: 400; font-display: swap; - src: local('Nunito Regular'), local('Nunito-Regular'), url(https://storage.googleapis.com/dusted-codes/fonts/Nunito/Nunito-Regular-cyrillic-ext.woff2) format('woff2'); + src: local('Nunito Regular'), local('Nunito-Regular'), url(https://cdn.dusted.codes/fonts/Nunito/Nunito-Regular-cyrillic-ext.woff2) format('woff2'); unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; } /* cyrillic */ @@ -157,7 +157,7 @@ font-style: normal; font-weight: 400; font-display: swap; - src: local('Nunito Regular'), local('Nunito-Regular'), url(https://storage.googleapis.com/dusted-codes/fonts/Nunito/Nunito-Regular-cyrillic.woff2) format('woff2'); + src: local('Nunito Regular'), local('Nunito-Regular'), url(https://cdn.dusted.codes/fonts/Nunito/Nunito-Regular-cyrillic.woff2) format('woff2'); unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; } /* vietnamese */ @@ -166,7 +166,7 @@ font-style: normal; font-weight: 400; font-display: swap; - src: local('Nunito Regular'), local('Nunito-Regular'), url(https://storage.googleapis.com/dusted-codes/fonts/Nunito/Nunito-Regular-vietnamese.woff2) format('woff2'); + src: local('Nunito Regular'), local('Nunito-Regular'), url(https://cdn.dusted.codes/fonts/Nunito/Nunito-Regular-vietnamese.woff2) format('woff2'); unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB; } /* latin-ext */ @@ -175,7 +175,7 @@ font-style: normal; font-weight: 400; font-display: swap; - src: local('Nunito Regular'), local('Nunito-Regular'), url(https://storage.googleapis.com/dusted-codes/fonts/Nunito/Nunito-Regular-latin-ext.woff2) format('woff2'); + src: local('Nunito Regular'), local('Nunito-Regular'), url(https://cdn.dusted.codes/fonts/Nunito/Nunito-Regular-latin-ext.woff2) format('woff2'); unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; } /* latin */ @@ -184,7 +184,7 @@ font-style: normal; font-weight: 400; font-display: swap; - src: local('Nunito Regular'), local('Nunito-Regular'), url(https://storage.googleapis.com/dusted-codes/fonts/Nunito/Nunito-Regular-latin.woff2) format('woff2'); + src: local('Nunito Regular'), local('Nunito-Regular'), url(https://cdn.dusted.codes/fonts/Nunito/Nunito-Regular-latin.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } /* cyrillic-ext */ @@ -193,7 +193,7 @@ font-style: normal; font-weight: 600; font-display: swap; - src: local('Nunito SemiBold'), local('Nunito-SemiBold'), url(https://storage.googleapis.com/dusted-codes/fonts/Nunito/Nunito-SemiBold-cyrillic-ext.woff2) format('woff2'); + src: local('Nunito SemiBold'), local('Nunito-SemiBold'), url(https://cdn.dusted.codes/fonts/Nunito/Nunito-SemiBold-cyrillic-ext.woff2) format('woff2'); unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; } /* cyrillic */ @@ -202,7 +202,7 @@ font-style: normal; font-weight: 600; font-display: swap; - src: local('Nunito SemiBold'), local('Nunito-SemiBold'), url(https://storage.googleapis.com/dusted-codes/fonts/Nunito/Nunito-SemiBold-cyrillic.woff2) format('woff2'); + src: local('Nunito SemiBold'), local('Nunito-SemiBold'), url(https://cdn.dusted.codes/fonts/Nunito/Nunito-SemiBold-cyrillic.woff2) format('woff2'); unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; } /* vietnamese */ @@ -211,7 +211,7 @@ font-style: normal; font-weight: 600; font-display: swap; - src: local('Nunito SemiBold'), local('Nunito-SemiBold'), url(https://storage.googleapis.com/dusted-codes/fonts/Nunito/Nunito-SemiBold-vietnamese.woff2) format('woff2'); + src: local('Nunito SemiBold'), local('Nunito-SemiBold'), url(https://cdn.dusted.codes/fonts/Nunito/Nunito-SemiBold-vietnamese.woff2) format('woff2'); unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB; } /* latin-ext */ @@ -220,7 +220,7 @@ font-style: normal; font-weight: 600; font-display: swap; - src: local('Nunito SemiBold'), local('Nunito-SemiBold'), url(https://storage.googleapis.com/dusted-codes/fonts/Nunito/Nunito-SemiBold-latin-ext.woff2) format('woff2'); + src: local('Nunito SemiBold'), local('Nunito-SemiBold'), url(https://cdn.dusted.codes/fonts/Nunito/Nunito-SemiBold-latin-ext.woff2) format('woff2'); unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; } /* latin */ @@ -229,7 +229,7 @@ font-style: normal; font-weight: 600; font-display: swap; - src: local('Nunito SemiBold'), local('Nunito-SemiBold'), url(https://storage.googleapis.com/dusted-codes/fonts/Nunito/Nunito-SemiBold-latin.woff2) format('woff2'); + src: local('Nunito SemiBold'), local('Nunito-SemiBold'), url(https://cdn.dusted.codes/fonts/Nunito/Nunito-SemiBold-latin.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } /* cyrillic-ext */ @@ -238,7 +238,7 @@ font-style: normal; font-weight: 700; font-display: swap; - src: local('Nunito Bold'), local('Nunito-Bold'), url(https://storage.googleapis.com/dusted-codes/fonts/Nunito/Nunito-Bold-cyrillic-ext.woff2) format('woff2'); + src: local('Nunito Bold'), local('Nunito-Bold'), url(https://cdn.dusted.codes/fonts/Nunito/Nunito-Bold-cyrillic-ext.woff2) format('woff2'); unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; } /* cyrillic */ @@ -247,7 +247,7 @@ font-style: normal; font-weight: 700; font-display: swap; - src: local('Nunito Bold'), local('Nunito-Bold'), url(https://storage.googleapis.com/dusted-codes/fonts/Nunito/Nunito-Bold-cyrillic.woff2) format('woff2'); + src: local('Nunito Bold'), local('Nunito-Bold'), url(https://cdn.dusted.codes/fonts/Nunito/Nunito-Bold-cyrillic.woff2) format('woff2'); unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; } /* vietnamese */ @@ -256,7 +256,7 @@ font-style: normal; font-weight: 700; font-display: swap; - src: local('Nunito Bold'), local('Nunito-Bold'), url(https://storage.googleapis.com/dusted-codes/fonts/Nunito/Nunito-Bold-vietnamese.woff2) format('woff2'); + src: local('Nunito Bold'), local('Nunito-Bold'), url(https://cdn.dusted.codes/fonts/Nunito/Nunito-Bold-vietnamese.woff2) format('woff2'); unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB; } /* latin-ext */ @@ -265,7 +265,7 @@ font-style: normal; font-weight: 700; font-display: swap; - src: local('Nunito Bold'), local('Nunito-Bold'), url(https://storage.googleapis.com/dusted-codes/fonts/Nunito/Nunito-Bold-latin-ext.woff2) format('woff2'); + src: local('Nunito Bold'), local('Nunito-Bold'), url(https://cdn.dusted.codes/fonts/Nunito/Nunito-Bold-latin-ext.woff2) format('woff2'); unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; } /* latin */ @@ -274,7 +274,7 @@ font-style: normal; font-weight: 700; font-display: swap; - src: local('Nunito Bold'), local('Nunito-Bold'), url(https://storage.googleapis.com/dusted-codes/fonts/Nunito/Nunito-Bold-latin.woff2) format('woff2'); + src: local('Nunito Bold'), local('Nunito-Bold'), url(https://cdn.dusted.codes/fonts/Nunito/Nunito-Bold-latin.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } /* cyrillic-ext */ @@ -283,7 +283,7 @@ font-style: normal; font-weight: 900; font-display: swap; - src: local('Nunito Black'), local('Nunito-Black'), url(https://storage.googleapis.com/dusted-codes/fonts/Nunito/Nunito-Black-cyrillic-ext.woff2) format('woff2'); + src: local('Nunito Black'), local('Nunito-Black'), url(https://cdn.dusted.codes/fonts/Nunito/Nunito-Black-cyrillic-ext.woff2) format('woff2'); unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; } /* cyrillic */ @@ -292,7 +292,7 @@ font-style: normal; font-weight: 900; font-display: swap; - src: local('Nunito Black'), local('Nunito-Black'), url(https://storage.googleapis.com/dusted-codes/fonts/Nunito/Nunito-Black-cyrillic.woff2) format('woff2'); + src: local('Nunito Black'), local('Nunito-Black'), url(https://cdn.dusted.codes/fonts/Nunito/Nunito-Black-cyrillic.woff2) format('woff2'); unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; } /* vietnamese */ @@ -301,7 +301,7 @@ font-style: normal; font-weight: 900; font-display: swap; - src: local('Nunito Black'), local('Nunito-Black'), url(https://storage.googleapis.com/dusted-codes/fonts/Nunito/Nunito-Black-vietnamese.woff2) format('woff2'); + src: local('Nunito Black'), local('Nunito-Black'), url(https://cdn.dusted.codes/fonts/Nunito/Nunito-Black-vietnamese.woff2) format('woff2'); unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB; } /* latin-ext */ @@ -310,7 +310,7 @@ font-style: normal; font-weight: 900; font-display: swap; - src: local('Nunito Black'), local('Nunito-Black'), url(https://storage.googleapis.com/dusted-codes/fonts/Nunito/Nunito-Black-latin-ext.woff2) format('woff2'); + src: local('Nunito Black'), local('Nunito-Black'), url(https://cdn.dusted.codes/fonts/Nunito/Nunito-Black-latin-ext.woff2) format('woff2'); unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; } /* latin */ @@ -319,7 +319,7 @@ font-style: normal; font-weight: 900; font-display: swap; - src: local('Nunito Black'), local('Nunito-Black'), url(https://storage.googleapis.com/dusted-codes/fonts/Nunito/Nunito-Black-latin.woff2) format('woff2'); + src: local('Nunito Black'), local('Nunito-Black'), url(https://cdn.dusted.codes/fonts/Nunito/Nunito-Black-latin.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } /* vietnamese */ @@ -328,7 +328,7 @@ font-style: italic; font-weight: 300; font-display: swap; - src: local('Nunito Sans Light Italic'), local('NunitoSans-LightItalic'), url(https://storage.googleapis.com/dusted-codes/fonts/NunitoSans/NunitoSans-LightItalic-vietnamese.woff2) format('woff2'); + src: local('Nunito Sans Light Italic'), local('NunitoSans-LightItalic'), url(https://cdn.dusted.codes/fonts/NunitoSans/NunitoSans-LightItalic-vietnamese.woff2) format('woff2'); unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB; } /* latin-ext */ @@ -337,7 +337,7 @@ font-style: italic; font-weight: 300; font-display: swap; - src: local('Nunito Sans Light Italic'), local('NunitoSans-LightItalic'), url(https://storage.googleapis.com/dusted-codes/fonts/NunitoSans/NunitoSans-LightItalic-latin-ext.woff2) format('woff2'); + src: local('Nunito Sans Light Italic'), local('NunitoSans-LightItalic'), url(https://cdn.dusted.codes/fonts/NunitoSans/NunitoSans-LightItalic-latin-ext.woff2) format('woff2'); unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; } /* latin */ @@ -346,7 +346,7 @@ font-style: italic; font-weight: 300; font-display: swap; - src: local('Nunito Sans Light Italic'), local('NunitoSans-LightItalic'), url(https://storage.googleapis.com/dusted-codes/fonts/NunitoSans/NunitoSans-LightItalic-latin.woff2) format('woff2'); + src: local('Nunito Sans Light Italic'), local('NunitoSans-LightItalic'), url(https://cdn.dusted.codes/fonts/NunitoSans/NunitoSans-LightItalic-latin.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } /* vietnamese */ @@ -355,7 +355,7 @@ font-style: italic; font-weight: 400; font-display: swap; - src: local('Nunito Sans Italic'), local('NunitoSans-Italic'), url(https://storage.googleapis.com/dusted-codes/fonts/NunitoSans/NunitoSans-Italic-vietnamese.woff2) format('woff2'); + src: local('Nunito Sans Italic'), local('NunitoSans-Italic'), url(https://cdn.dusted.codes/fonts/NunitoSans/NunitoSans-Italic-vietnamese.woff2) format('woff2'); unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB; } /* latin-ext */ @@ -364,7 +364,7 @@ font-style: italic; font-weight: 400; font-display: swap; - src: local('Nunito Sans Italic'), local('NunitoSans-Italic'), url(https://storage.googleapis.com/dusted-codes/fonts/NunitoSans/NunitoSans-Italic-latin-ext.woff2) format('woff2'); + src: local('Nunito Sans Italic'), local('NunitoSans-Italic'), url(https://cdn.dusted.codes/fonts/NunitoSans/NunitoSans-Italic-latin-ext.woff2) format('woff2'); unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; } /* latin */ @@ -373,7 +373,7 @@ font-style: italic; font-weight: 400; font-display: swap; - src: local('Nunito Sans Italic'), local('NunitoSans-Italic'), url(https://storage.googleapis.com/dusted-codes/fonts/NunitoSans/NunitoSans-Italic-latin.woff2) format('woff2'); + src: local('Nunito Sans Italic'), local('NunitoSans-Italic'), url(https://cdn.dusted.codes/fonts/NunitoSans/NunitoSans-Italic-latin.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } /* vietnamese */ @@ -382,7 +382,7 @@ font-style: normal; font-weight: 300; font-display: swap; - src: local('Nunito Sans Light'), local('NunitoSans-Light'), url(https://storage.googleapis.com/dusted-codes/fonts/NunitoSans/NunitoSans-Light-vietnamese.woff2) format('woff2'); + src: local('Nunito Sans Light'), local('NunitoSans-Light'), url(https://cdn.dusted.codes/fonts/NunitoSans/NunitoSans-Light-vietnamese.woff2) format('woff2'); unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB; } /* latin-ext */ @@ -391,7 +391,7 @@ font-style: normal; font-weight: 300; font-display: swap; - src: local('Nunito Sans Light'), local('NunitoSans-Light'), url(https://storage.googleapis.com/dusted-codes/fonts/NunitoSans/NunitoSans-Light-latin-ext.woff2) format('woff2'); + src: local('Nunito Sans Light'), local('NunitoSans-Light'), url(https://cdn.dusted.codes/fonts/NunitoSans/NunitoSans-Light-latin-ext.woff2) format('woff2'); unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; } /* latin */ @@ -400,7 +400,7 @@ font-style: normal; font-weight: 300; font-display: swap; - src: local('Nunito Sans Light'), local('NunitoSans-Light'), url(https://storage.googleapis.com/dusted-codes/fonts/NunitoSans/NunitoSans-Light-latin.woff2) format('woff2'); + src: local('Nunito Sans Light'), local('NunitoSans-Light'), url(https://cdn.dusted.codes/fonts/NunitoSans/NunitoSans-Light-latin.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } /* vietnamese */ @@ -409,7 +409,7 @@ font-style: normal; font-weight: 400; font-display: swap; - src: local('Nunito Sans Regular'), local('NunitoSans-Regular'), url(https://storage.googleapis.com/dusted-codes/fonts/NunitoSans/NunitoSans-Regular-vietnamese.woff2) format('woff2'); + src: local('Nunito Sans Regular'), local('NunitoSans-Regular'), url(https://cdn.dusted.codes/fonts/NunitoSans/NunitoSans-Regular-vietnamese.woff2) format('woff2'); unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB; } /* latin-ext */ @@ -418,7 +418,7 @@ font-style: normal; font-weight: 400; font-display: swap; - src: local('Nunito Sans Regular'), local('NunitoSans-Regular'), url(https://storage.googleapis.com/dusted-codes/fonts/NunitoSans/NunitoSans-Regular-latin-ext.woff2) format('woff2'); + src: local('Nunito Sans Regular'), local('NunitoSans-Regular'), url(https://cdn.dusted.codes/fonts/NunitoSans/NunitoSans-Regular-latin-ext.woff2) format('woff2'); unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; } /* latin */ @@ -427,7 +427,7 @@ font-style: normal; font-weight: 400; font-display: swap; - src: local('Nunito Sans Regular'), local('NunitoSans-Regular'), url(https://storage.googleapis.com/dusted-codes/fonts/NunitoSans/NunitoSans-Regular-latin.woff2) format('woff2'); + src: local('Nunito Sans Regular'), local('NunitoSans-Regular'), url(https://cdn.dusted.codes/fonts/NunitoSans/NunitoSans-Regular-latin.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } /* vietnamese */ @@ -436,7 +436,7 @@ font-style: normal; font-weight: 600; font-display: swap; - src: local('Nunito Sans SemiBold'), local('NunitoSans-SemiBold'), url(https://storage.googleapis.com/dusted-codes/fonts/NunitoSans/NunitoSans-SemiBold-vietnamese.woff2) format('woff2'); + src: local('Nunito Sans SemiBold'), local('NunitoSans-SemiBold'), url(https://cdn.dusted.codes/fonts/NunitoSans/NunitoSans-SemiBold-vietnamese.woff2) format('woff2'); unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB; } /* latin-ext */ @@ -445,7 +445,7 @@ font-style: normal; font-weight: 600; font-display: swap; - src: local('Nunito Sans SemiBold'), local('NunitoSans-SemiBold'), url(https://storage.googleapis.com/dusted-codes/fonts/NunitoSans/NunitoSans-SemiBold-latin-ext.woff2) format('woff2'); + src: local('Nunito Sans SemiBold'), local('NunitoSans-SemiBold'), url(https://cdn.dusted.codes/fonts/NunitoSans/NunitoSans-SemiBold-latin-ext.woff2) format('woff2'); unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; } /* latin */ @@ -454,7 +454,7 @@ font-style: normal; font-weight: 600; font-display: swap; - src: local('Nunito Sans SemiBold'), local('NunitoSans-SemiBold'), url(https://storage.googleapis.com/dusted-codes/fonts/NunitoSans/NunitoSans-SemiBold-latin.woff2) format('woff2'); + src: local('Nunito Sans SemiBold'), local('NunitoSans-SemiBold'), url(https://cdn.dusted.codes/fonts/NunitoSans/NunitoSans-SemiBold-latin.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } /* vietnamese */ @@ -463,7 +463,7 @@ font-style: normal; font-weight: 700; font-display: swap; - src: local('Nunito Sans Bold'), local('NunitoSans-Bold'), url(https://storage.googleapis.com/dusted-codes/fonts/NunitoSans/NunitoSans-Bold-vietnamese.woff2) format('woff2'); + src: local('Nunito Sans Bold'), local('NunitoSans-Bold'), url(https://cdn.dusted.codes/fonts/NunitoSans/NunitoSans-Bold-vietnamese.woff2) format('woff2'); unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB; } /* latin-ext */ @@ -472,7 +472,7 @@ font-style: normal; font-weight: 700; font-display: swap; - src: local('Nunito Sans Bold'), local('NunitoSans-Bold'), url(https://storage.googleapis.com/dusted-codes/fonts/NunitoSans/NunitoSans-Bold-latin-ext.woff2) format('woff2'); + src: local('Nunito Sans Bold'), local('NunitoSans-Bold'), url(https://cdn.dusted.codes/fonts/NunitoSans/NunitoSans-Bold-latin-ext.woff2) format('woff2'); unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; } /* latin */ @@ -481,7 +481,7 @@ font-style: normal; font-weight: 700; font-display: swap; - src: local('Nunito Sans Bold'), local('NunitoSans-Bold'), url(https://storage.googleapis.com/dusted-codes/fonts/NunitoSans/NunitoSans-Bold-latin.woff2) format('woff2'); + src: local('Nunito Sans Bold'), local('NunitoSans-Bold'), url(https://cdn.dusted.codes/fonts/NunitoSans/NunitoSans-Bold-latin.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } /* cyrillic-ext */ @@ -490,7 +490,7 @@ font-style: italic; font-weight: 400; font-display: swap; - src: local('Roboto Mono Italic'), local('RobotoMono-Italic'), url(https://storage.googleapis.com/dusted-codes/fonts/RobotoMono/RobotoMono-Italic-cyrillic-ext.woff2) format('woff2'); + src: local('Roboto Mono Italic'), local('RobotoMono-Italic'), url(https://cdn.dusted.codes/fonts/RobotoMono/RobotoMono-Italic-cyrillic-ext.woff2) format('woff2'); unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; } /* cyrillic */ @@ -499,7 +499,7 @@ font-style: italic; font-weight: 400; font-display: swap; - src: local('Roboto Mono Italic'), local('RobotoMono-Italic'), url(https://storage.googleapis.com/dusted-codes/fonts/RobotoMono/RobotoMono-Italic-cyrillic.woff2) format('woff2'); + src: local('Roboto Mono Italic'), local('RobotoMono-Italic'), url(https://cdn.dusted.codes/fonts/RobotoMono/RobotoMono-Italic-cyrillic.woff2) format('woff2'); unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; } /* greek-ext */ @@ -508,7 +508,7 @@ font-style: italic; font-weight: 400; font-display: swap; - src: local('Roboto Mono Italic'), local('RobotoMono-Italic'), url(https://storage.googleapis.com/dusted-codes/fonts/RobotoMono/RobotoMono-Italic-greek-ext.woff2) format('woff2'); + src: local('Roboto Mono Italic'), local('RobotoMono-Italic'), url(https://cdn.dusted.codes/fonts/RobotoMono/RobotoMono-Italic-greek-ext.woff2) format('woff2'); unicode-range: U+1F00-1FFF; } /* greek */ @@ -517,7 +517,7 @@ font-style: italic; font-weight: 400; font-display: swap; - src: local('Roboto Mono Italic'), local('RobotoMono-Italic'), url(https://storage.googleapis.com/dusted-codes/fonts/RobotoMono/RobotoMono-Italic-greek.woff2) format('woff2'); + src: local('Roboto Mono Italic'), local('RobotoMono-Italic'), url(https://cdn.dusted.codes/fonts/RobotoMono/RobotoMono-Italic-greek.woff2) format('woff2'); unicode-range: U+0370-03FF; } /* vietnamese */ @@ -526,7 +526,7 @@ font-style: italic; font-weight: 400; font-display: swap; - src: local('Roboto Mono Italic'), local('RobotoMono-Italic'), url(https://storage.googleapis.com/dusted-codes/fonts/RobotoMono/RobotoMono-Italic-vietnamese.woff2) format('woff2'); + src: local('Roboto Mono Italic'), local('RobotoMono-Italic'), url(https://cdn.dusted.codes/fonts/RobotoMono/RobotoMono-Italic-vietnamese.woff2) format('woff2'); unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB; } /* latin-ext */ @@ -535,7 +535,7 @@ font-style: italic; font-weight: 400; font-display: swap; - src: local('Roboto Mono Italic'), local('RobotoMono-Italic'), url(https://storage.googleapis.com/dusted-codes/fonts/RobotoMono/RobotoMono-Italic-latin-ext.woff2) format('woff2'); + src: local('Roboto Mono Italic'), local('RobotoMono-Italic'), url(https://cdn.dusted.codes/fonts/RobotoMono/RobotoMono-Italic-latin-ext.woff2) format('woff2'); unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; } /* latin */ @@ -544,7 +544,7 @@ font-style: italic; font-weight: 400; font-display: swap; - src: local('Roboto Mono Italic'), local('RobotoMono-Italic'), url(https://storage.googleapis.com/dusted-codes/fonts/RobotoMono/RobotoMono-Italic-latin.woff2) format('woff2'); + src: local('Roboto Mono Italic'), local('RobotoMono-Italic'), url(https://cdn.dusted.codes/fonts/RobotoMono/RobotoMono-Italic-latin.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } /* cyrillic-ext */ @@ -553,7 +553,7 @@ font-style: normal; font-weight: 400; font-display: swap; - src: local('Roboto Mono'), local('RobotoMono-Regular'), url(https://storage.googleapis.com/dusted-codes/fonts/RobotoMono/RobotoMono-Regular-cyrillic-ext.woff2) format('woff2'); + src: local('Roboto Mono'), local('RobotoMono-Regular'), url(https://cdn.dusted.codes/fonts/RobotoMono/RobotoMono-Regular-cyrillic-ext.woff2) format('woff2'); unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; } /* cyrillic */ @@ -562,7 +562,7 @@ font-style: normal; font-weight: 400; font-display: swap; - src: local('Roboto Mono'), local('RobotoMono-Regular'), url(https://storage.googleapis.com/dusted-codes/fonts/RobotoMono/RobotoMono-Regular-cyrillic.woff2) format('woff2'); + src: local('Roboto Mono'), local('RobotoMono-Regular'), url(https://cdn.dusted.codes/fonts/RobotoMono/RobotoMono-Regular-cyrillic.woff2) format('woff2'); unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; } /* greek-ext */ @@ -571,7 +571,7 @@ font-style: normal; font-weight: 400; font-display: swap; - src: local('Roboto Mono'), local('RobotoMono-Regular'), url(https://storage.googleapis.com/dusted-codes/fonts/RobotoMono/RobotoMono-Regular-greek-ext.woff2) format('woff2'); + src: local('Roboto Mono'), local('RobotoMono-Regular'), url(https://cdn.dusted.codes/fonts/RobotoMono/RobotoMono-Regular-greek-ext.woff2) format('woff2'); unicode-range: U+1F00-1FFF; } /* greek */ @@ -580,7 +580,7 @@ font-style: normal; font-weight: 400; font-display: swap; - src: local('Roboto Mono'), local('RobotoMono-Regular'), url(https://storage.googleapis.com/dusted-codes/fonts/RobotoMono/RobotoMono-Regular-greek.woff2) format('woff2'); + src: local('Roboto Mono'), local('RobotoMono-Regular'), url(https://cdn.dusted.codes/fonts/RobotoMono/RobotoMono-Regular-greek.woff2) format('woff2'); unicode-range: U+0370-03FF; } /* vietnamese */ @@ -589,7 +589,7 @@ font-style: normal; font-weight: 400; font-display: swap; - src: local('Roboto Mono'), local('RobotoMono-Regular'), url(https://storage.googleapis.com/dusted-codes/fonts/RobotoMono/RobotoMono-Regular-vietnamese.woff2) format('woff2'); + src: local('Roboto Mono'), local('RobotoMono-Regular'), url(https://cdn.dusted.codes/fonts/RobotoMono/RobotoMono-Regular-vietnamese.woff2) format('woff2'); unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB; } /* latin-ext */ @@ -598,7 +598,7 @@ font-style: normal; font-weight: 400; font-display: swap; - src: local('Roboto Mono'), local('RobotoMono-Regular'), url(https://storage.googleapis.com/dusted-codes/fonts/RobotoMono/RobotoMono-Regular-latin-ext.woff2) format('woff2'); + src: local('Roboto Mono'), local('RobotoMono-Regular'), url(https://cdn.dusted.codes/fonts/RobotoMono/RobotoMono-Regular-latin-ext.woff2) format('woff2'); unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; } /* latin */ @@ -607,6 +607,6 @@ font-style: normal; font-weight: 400; font-display: swap; - src: local('Roboto Mono'), local('RobotoMono-Regular'), url(https://storage.googleapis.com/dusted-codes/fonts/RobotoMono/RobotoMono-Regular-latin.woff2) format('woff2'); + src: local('Roboto Mono'), local('RobotoMono-Regular'), url(https://cdn.dusted.codes/fonts/RobotoMono/RobotoMono-Regular-latin.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } \ No newline at end of file From 0757d8d897b1bdf98f3dfe8d6285aca797595c03 Mon Sep 17 00:00:00 2001 From: Dustin Moris Gorski Date: Mon, 12 Apr 2021 22:26:31 +0100 Subject: [PATCH 4/7] .NET 6 preview fix --- src/DustedCodes/DustedCodes.fsproj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/DustedCodes/DustedCodes.fsproj b/src/DustedCodes/DustedCodes.fsproj index fa89063..adfa348 100644 --- a/src/DustedCodes/DustedCodes.fsproj +++ b/src/DustedCodes/DustedCodes.fsproj @@ -2,6 +2,7 @@ net5.0 + Major DustedCodes true false From bc77b2d02acb8d02857953622773b845d48ed49c Mon Sep 17 00:00:00 2001 From: Dustin Moris Gorski Date: Mon, 12 Apr 2021 22:29:09 +0100 Subject: [PATCH 5/7] cancellation token support --- src/DustedCodes/Captcha.fs | 8 +++++--- src/DustedCodes/Helpers.fs | 26 ++++++++++++++++++-------- src/DustedCodes/HttpHandlers.fs | 8 +++++--- src/DustedCodes/Messages.fs | 7 ++++--- 4 files changed, 32 insertions(+), 17 deletions(-) diff --git a/src/DustedCodes/Captcha.fs b/src/DustedCodes/Captcha.fs index a64e82e..b2664bd 100644 --- a/src/DustedCodes/Captcha.fs +++ b/src/DustedCodes/Captcha.fs @@ -4,6 +4,7 @@ namespace DustedCodes module Captcha = open System open System.Net + open System.Threading open System.Threading.Tasks open FSharp.Control.Tasks.NonAffine open Newtonsoft.Json @@ -40,18 +41,19 @@ module Captcha = UserError "Verification failed. Please try again." | _ -> ServerError (sprintf "Unknown error code: %s" errorCode) - type ValidateFunc = string -> string -> string -> Task + type ValidateFunc = string -> string -> string -> CancellationToken -> Task let validate (postCaptcha : Http.PostFormFunc) (siteKey : string) (secretKey : string) - (captchaResponse : string) = + (captchaResponse : string) + (ct : CancellationToken) = task { let data = dict [ "siteKey", siteKey "secret", secretKey "response", captchaResponse ] - let! result = postCaptcha data + let! result = postCaptcha data ct return match result with | Error err -> ServerError err diff --git a/src/DustedCodes/Helpers.fs b/src/DustedCodes/Helpers.fs index 17bf3d1..0d5bfae 100644 --- a/src/DustedCodes/Helpers.fs +++ b/src/DustedCodes/Helpers.fs @@ -55,19 +55,23 @@ module Network = module Http = open System.Text open System.Net.Http + open System.Threading open System.Threading.Tasks open System.Collections.Generic open FSharp.Control.Tasks.NonAffine open Newtonsoft.Json type PostResult = Task> - type PostFormFunc = IDictionary -> PostResult - type PostJsonFunc = obj -> PostResult + type PostFormFunc = IDictionary -> CancellationToken -> PostResult + type PostJsonFunc = obj -> CancellationToken -> PostResult - let private postReq (client : HttpClient) (req : HttpRequestMessage) : PostResult = + let private postReq + (client : HttpClient) + (req : HttpRequestMessage) + (ct : CancellationToken) : PostResult = task { try - let! resp = client.SendAsync req + let! resp = client.SendAsync(req, ct) let! body = resp.Content.ReadAsStringAsync() return match resp.IsSuccessStatusCode with @@ -78,19 +82,25 @@ module Http = return Error ex.Message } - let postForm (client : HttpClient) (form : IDictionary) = + let postForm + (client : HttpClient) + (form : IDictionary) + (ct : CancellationToken) = task { use data = new FormUrlEncodedContent(form) use req = new HttpRequestMessage(Method = HttpMethod.Post, Content = data) - return! postReq client req + return! postReq client req ct } - let postJson (client : HttpClient) (data : obj) = + let postJson + (client : HttpClient) + (data : obj) + (ct : CancellationToken) = task { let json = JsonConvert.SerializeObject data use data = new StringContent(json, Encoding.UTF8, "application/json") use req = new HttpRequestMessage(Method = HttpMethod.Post, Content = data) - return! postReq client req + return! postReq client req ct } // --------------------------------- diff --git a/src/DustedCodes/HttpHandlers.fs b/src/DustedCodes/HttpHandlers.fs index f65c800..c142184 100644 --- a/src/DustedCodes/HttpHandlers.fs +++ b/src/DustedCodes/HttpHandlers.fs @@ -117,12 +117,14 @@ module HttpHandlers = match msg.IsValid with | Error err -> return! respond msg (Error err) | Ok _ -> + let captchaResponse = ctx.Request.Form.["h-captcha-response"].ToString() let validateCaptcha = ctx.GetService() let! captchaResult = - ctx.Request.Form.["h-captcha-response"].ToString() - |> validateCaptcha + validateCaptcha settings.ThirdParties.CaptchaSiteKey settings.ThirdParties.CaptchaSecretKey + captchaResponse + ctx.RequestAborted match captchaResult with | Captcha.ServerError err -> log Level.Critical @@ -132,7 +134,7 @@ module HttpHandlers = | Captcha.Success -> let saveMsg = ctx.GetService() let timer = Stopwatch.StartNew() - let! result = saveMsg log msg + let! result = saveMsg log msg ctx.RequestAborted timer.Stop() log Level.Debug (sprintf "Sent message in %s" (timer.Elapsed.ToMs())) match result with diff --git a/src/DustedCodes/Messages.fs b/src/DustedCodes/Messages.fs index ef5fecb..3e6e71f 100644 --- a/src/DustedCodes/Messages.fs +++ b/src/DustedCodes/Messages.fs @@ -4,6 +4,7 @@ namespace DustedCodes module Messages = open System open System.Collections.Generic + open System.Threading open System.Threading.Tasks open FSharp.Control.Tasks.NonAffine @@ -71,7 +72,7 @@ module Messages = ] } : Request - type SaveFunc = Log.Func -> ContactMsg -> Task> + type SaveFunc = Log.Func -> ContactMsg -> CancellationToken -> Task> let save (postJson : Http.PostJsonFunc) @@ -79,10 +80,10 @@ module Messages = (domain : string) (sender : string) (recipient : string) = - fun (log : Log.Func) (msg : ContactMsg) -> + fun (log : Log.Func) (msg : ContactMsg) (ct : CancellationToken) -> task { let data = msg.ToRequest envName domain sender recipient - let! result = postJson data + let! result = postJson data ct return match result with | Ok _ -> Ok "Thank you, your message has been successfully sent!" From 45179f2663e33ebf3003e046cbfa78cfdef597b4 Mon Sep 17 00:00:00 2001 From: Dustin Moris Gorski Date: Mon, 12 Apr 2021 22:30:58 +0100 Subject: [PATCH 6/7] new blog post --- src/DustedCodes/BlogPosts/2021_03_22-new.md | 59 +++++++++++++++++++++ src/DustedCodes/CSS/site.css | 31 +++++++++++ 2 files changed, 90 insertions(+) create mode 100644 src/DustedCodes/BlogPosts/2021_03_22-new.md diff --git a/src/DustedCodes/BlogPosts/2021_03_22-new.md b/src/DustedCodes/BlogPosts/2021_03_22-new.md new file mode 100644 index 0000000..6709cce --- /dev/null +++ b/src/DustedCodes/BlogPosts/2021_03_22-new.md @@ -0,0 +1,59 @@ + + +# You don’t need Docker + +*You don’t need Docker. I started my business on a small server under my desk. It took me more than 10 years before I reached the scale where I needed something like Docker. You’ll be fine for a very long time before you’ll have to worry about it!* + +![You don't need Docker](https://cdn.dusted.codes/images/blog-posts/2021-03-26/you-dont-need-docker.png) + +Who has never heard someone say something like this? + +[I hear it all the time](https://dev.to/inductor/do-you-really-need-docker-or-kubernetes-in-your-system-11nk). At least once a week I see someone [tweet](https://twitter.com/FransBouma/status/1216736461166383105?s=20) or [blog](https://medium.com/@chintanaw/no-you-dont-need-cloud-docker-no-kubernetes-hell-no-ae2e422d0942) about how [Docker is not really needed](https://launchyourapp.meezeeworkouts.com/2021/03/why-we-dont-use-docker-we-dont-need-it.html?m=1) and how they managed to get away without it. To be honest they are not wrong. Nobody really *needs* Docker, but then again “need” is a very strong word. The real question is “Do you want Docker”? + +## A different world + +It is true, many successful websites and web applications started without Docker. Many also started without the “Cloud”. Some probably even started without NoSQL databases (there was a time when MySQL and Oracle were king), no Redis, no SendGrid or MailChimp, no Stripe or Braintree, no Angular or React, definitely no Vue, no serverless functions, no queues and distributed systems, no CSS frameworks, no CI/CD pipelines and probably not even virtual machines! Heck, some websites probably didn't even use jQuery or JavaScript to begin with! + +However, would anyone really want to start a new internet business without these tools today? I don't think so, at least not if they aim for success! + +It's easy to forget but when companies like MySpace or Facebook started the world was a very different place. We had no social media (that’s kind of obvious from this example), no iPads and iPhones, no smartwatches, no home speakers, we had no fibre optic cables leading to our homes and we didn’t have mobile broadband either. The aspiration of internet domination wasn’t a thing yet. Even when Facebook already reached huge success they were still only one of many other social networks on the web. We had many individual (often national) versions of something similar to Facebook for a very long time before the world wide web became a little bit less wide again. + +The world, our relationship with the internet and people's expectations were completely different than what they are today. + +### Instant scale was not a threat + +Do you remember [online bulletin boards (BBS)](https://en.wikipedia.org/wiki/Bulletin_board_system)? Before Reddit we had many independent self-hosted internet forums. There was a time where there was no WordPress or Medium. The internet started off as a truly decentralised web with many independent websites, blogs, communities and even multiple search engines before Google took over. Internet users didn't congregate in the same places. + +**Websites had time to grow**. The danger for an indy blog seeing traffic spikes from 3 users per week to tens of thousands of users in a single day was just not a credible threat. Today it doesn't matter how fringe or unknown a website is, any page could suddenly end up on Hacker News and learn what it means to get the infamous [Hacker News hug of death](https://www.indiehackers.com/post/the-hacker-news-hug-50-000-unique-visitors-in-18-hours-65977e0636). Maybe a few years ago it was possible to delay the thought of scale to a later stage, but today this is not possible if one wants to reap the benefits of sudden success. + +### A more patient crowd + +The first time I used an instant messenger was at the time of [ICQ](https://en.wikipedia.org/wiki/ICQ). The first time I downloaded music was from [Napster](https://en.wikipedia.org/wiki/Napster). Then I switched to [KaZaA](https://en.wikipedia.org/wiki/Kazaa), then [LimeWire](https://en.wikipedia.org/wiki/LimeWire), then [eDonkey](https://en.wikipedia.org/wiki/EDonkey2000) and later to [Giganews](https://en.wikipedia.org/wiki/Giganews) which was a popular [Usenet](https://en.wikipedia.org/wiki/Usenet) at that time. What they all had in common was the true nature of a decentralised web. They were all built on so called [peer-to-peer networks](https://en.wikipedia.org/wiki/Peer-to-peer). When my friends went offline then I couldn't send them a message any more. Messages just wouldn't arrive. There would be no connection and it would time out. When enough "peers" turned their computers off then my downloads would pause. It was totally normal to download content over a period of multiple days if not weeks. There was absolutely no expectation for things to happen in an instant moment. + +Nowadays nobody would accept a download to take longer than a few seconds let alone a couple weeks. Patience has come down and expectations went up high. What was once a luxury experience is now the baseline bar. If a video doesn't stream in at least 1080p then it might as well never happen. If the quality is not right then people turn away. Startups, indy hackers, open source projects and even hobbyists cannot afford to offer a degraded service if they want to get traction in current times. + +### The internet was a toy + +When I got my first computer the internet was nothing more than a toy. My relationships with my family and friends did not rely on the availability of WhatsApp. Jobs were not impacted if StackOverflow, GitHub or Slack were a bit slow. Today I can't even book a doctor's appointment without going online. + +As we have become increasingly more dependent on the web, service providers have gained a higher responsibility in keeping their services alive. Today it's rather questionable if a business can still offer a meaningful SLA with a server under someone's desk. + +### Tolerance towards failure + +Tolerance towards failure is another benefit which web applications had in the past. Today not so much anymore. Nobody expects Uber to be down. Binge watching is only possible because Netflix never goes offline. Music never stops playing when you're on Spotify. And if it did then people would lash out. + +We've become so used to the high quality and availability of services that no glitches in the system go unnoticed anymore. No failure, no scalability issue and no data loss get past users without people grinding their teeth or writing angry tweets. Every incident has a lasting effect and can limit one's future potential of growth. For example, I have never hosted anything on GitLab myself but I sure know that they are infamous for [being](https://github.com/sameersbn/docker-gitlab/issues/13) [so](https://serverfault.com/questions/1049621/gitlab-push-very-slow-gitlab-ce) [awfully](https://gitlabfan.com/why-gitlab-is-slow-and-what-you-can-do-about-it-bca9d61405bd) [slow](https://stackoverflow.com/questions/43226191/frequently-our-gitlab-is-getting-slow) or losing [production data](https://gitlab.developers.cam.ac.uk/uis/devops/devhub/docs/-/wikis/reports/29th-March-2019-Incident-Report) without full recourse. + +### The network effect + +Although everyone likes some quick gains, nobody likes to see their business outgrow their own ability of keeping up with demand. **Scale plays a huge part in that realm.** We are so interconnected that unless someone launches a product in a private invite-only group they won't be able to predict (or control) how fast they will grow. The internet has its own mind and nobody knows who will be famous tomorrow and what will go viral today. An innocent tweet, a [short post on Reddit](https://remoteclan.com/s/27ihu5/my_product_scale_went_viral_150_000_views) or a routine launch on Product Hunt can [shift a new startup from 0 to 10,000 users](https://medium.com/@vinayh/0-10-000-users-how-openvid-launched-on-product-hunt-575ff9ecf7a1) in the span of 3 months. That level of virality is insane. Imagine having 10,000 willing customers on your doorstep but they can't get in because someone told you that you won't have to think about scale for a while. Don't become victim of your own success. + +## Do you need want Docker? + +Nobody really needs Docker (or containers per se) and I'm not going to claim that containers are the perfect silver bullet solution to all the issues from above, but it remains an incredibly powerful tool which can address many of today's challenges in a very time and cost effective way. Sure there is an upfront investment to be made in learning container technology and putting it into practice, but by no means is it any harder or more time intensive than learning a new CSS framework or the latest flavour of JS. If anything skills like Docker are much more generic and more broadly transferable between tech stacks, programming languages and jobs. + +Containers make builds predictable, it makes deployments reliable and it makes horizontal scaling a breeze. Containers are a great way of providing stable and backwards compatible APIs whilst keeping code complexity low. Containers can reduce infrastructure cost by running multiple applications on the same box. They can accelerate a team's productivity by running different feature branches at the same time and scale testing environments independent of hardware. At last containers make blue green deployments easy and help to keep downtime low. + +Therefore, my question is, do you want Docker? \ No newline at end of file diff --git a/src/DustedCodes/CSS/site.css b/src/DustedCodes/CSS/site.css index 70eb516..b9a222a 100644 --- a/src/DustedCodes/CSS/site.css +++ b/src/DustedCodes/CSS/site.css @@ -148,6 +148,37 @@ kbd { font-size: .85em; } +del { + text-decoration: none; + position: relative; +} + +del:before { + content: ' '; + font-size: inherit; + display: block; + position: absolute; + right: 0; + left: 0; + top: 50%; + bottom: 50%; + border-top: .075em solid var(--error-bg-color); + transform: rotate(10deg); +} + +del:after { + content: ' '; + font-size: inherit; + display: block; + position: absolute; + right: 0; + left: 0; + top: 50%; + bottom: 50%; + border-top: .075em solid var(--error-bg-color); + transform: rotate(-10deg); +} + table { display: block; } From 62ac6b09d5cc844bb9c42797aa205eeda953cb3c Mon Sep 17 00:00:00 2001 From: Dustin Moris Gorski Date: Mon, 12 Apr 2021 23:09:02 +0100 Subject: [PATCH 7/7] blog post edits --- ...new.md => 2021_04_12-you-dont-need-docker.md} | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) rename src/DustedCodes/BlogPosts/{2021_03_22-new.md => 2021_04_12-you-dont-need-docker.md} (76%) diff --git a/src/DustedCodes/BlogPosts/2021_03_22-new.md b/src/DustedCodes/BlogPosts/2021_04_12-you-dont-need-docker.md similarity index 76% rename from src/DustedCodes/BlogPosts/2021_03_22-new.md rename to src/DustedCodes/BlogPosts/2021_04_12-you-dont-need-docker.md index 6709cce..a3bd60f 100644 --- a/src/DustedCodes/BlogPosts/2021_03_22-new.md +++ b/src/DustedCodes/BlogPosts/2021_04_12-you-dont-need-docker.md @@ -1,9 +1,11 @@ # You don’t need Docker +
      Note: In this blog post I use the terms Docker and containers interchangeably. I know that Docker is only one of many container technologies and not always the best suited one (e.g. Kubernetes), but for the purpose of this blog post I don't differentiate between them.
      + *You don’t need Docker. I started my business on a small server under my desk. It took me more than 10 years before I reached the scale where I needed something like Docker. You’ll be fine for a very long time before you’ll have to worry about it!* ![You don't need Docker](https://cdn.dusted.codes/images/blog-posts/2021-03-26/you-dont-need-docker.png) @@ -18,13 +20,13 @@ It is true, many successful websites and web applications started without Docker However, would anyone really want to start a new internet business without these tools today? I don't think so, at least not if they aim for success! -It's easy to forget but when companies like MySpace or Facebook started the world was a very different place. We had no social media (that’s kind of obvious from this example), no iPads and iPhones, no smartwatches, no home speakers, we had no fibre optic cables leading to our homes and we didn’t have mobile broadband either. The aspiration of internet domination wasn’t a thing yet. Even when Facebook already reached huge success they were still only one of many other social networks on the web. We had many individual (often national) versions of something similar to Facebook for a very long time before the world wide web became a little bit less wide again. +It's easy to forget but when companies like MySpace or Facebook started the world was a very different place. We had no social media (that’s kind of obvious from this example), no iPads and iPhones, no smartwatches, no home speakers, we had no fibre optic cables leading to our homes and we didn’t have mobile broadband either. The aspiration of internet domination wasn’t a thing yet. Even when Facebook already reached huge success they were still only one of many other social networks on the web. We had many individual (often national) versions of something similar to Facebook for a very long time before the world wide web became a little bit less wide again. Internet usage was very different too. [Psy didn't even break YouTube yet](https://www.telegraph.co.uk/technology/news/11272577/How-South-Korean-pop-star-Psy-broke-YouTube.html). The world, our relationship with the internet and people's expectations were completely different than what they are today. ### Instant scale was not a threat -Do you remember [online bulletin boards (BBS)](https://en.wikipedia.org/wiki/Bulletin_board_system)? Before Reddit we had many independent self-hosted internet forums. There was a time where there was no WordPress or Medium. The internet started off as a truly decentralised web with many independent websites, blogs, communities and even multiple search engines before Google took over. Internet users didn't congregate in the same places. +Do you remember [online bulletin boards (BBS)](https://en.wikipedia.org/wiki/Bulletin_board_system)? Before Reddit we had many independent self-hosted internet forums. There was a time where there was no WordPress or Medium. The internet started off as a truly decentralised web with many independent websites, blogs, communities and even multiple search engines before Google took over. Internet users didn't all congregate in the same places then. **Websites had time to grow**. The danger for an indy blog seeing traffic spikes from 3 users per week to tens of thousands of users in a single day was just not a credible threat. Today it doesn't matter how fringe or unknown a website is, any page could suddenly end up on Hacker News and learn what it means to get the infamous [Hacker News hug of death](https://www.indiehackers.com/post/the-hacker-news-hug-50-000-unique-visitors-in-18-hours-65977e0636). Maybe a few years ago it was possible to delay the thought of scale to a later stage, but today this is not possible if one wants to reap the benefits of sudden success. @@ -48,12 +50,12 @@ We've become so used to the high quality and availability of services that no gl ### The network effect -Although everyone likes some quick gains, nobody likes to see their business outgrow their own ability of keeping up with demand. **Scale plays a huge part in that realm.** We are so interconnected that unless someone launches a product in a private invite-only group they won't be able to predict (or control) how fast they will grow. The internet has its own mind and nobody knows who will be famous tomorrow and what will go viral today. An innocent tweet, a [short post on Reddit](https://remoteclan.com/s/27ihu5/my_product_scale_went_viral_150_000_views) or a routine launch on Product Hunt can [shift a new startup from 0 to 10,000 users](https://medium.com/@vinayh/0-10-000-users-how-openvid-launched-on-product-hunt-575ff9ecf7a1) in the span of 3 months. That level of virality is insane. Imagine having 10,000 willing customers on your doorstep but they can't get in because someone told you that you won't have to think about scale for a while. Don't become victim of your own success. +Although everyone likes some quick gains, nobody likes to see their business outgrow their own ability of keeping up with demand. **Scale plays a huge part in that realm.** We are so interconnected that unless someone launches a product in a private invite-only group they won't be able to predict (or control) how fast they will grow. The internet has its own mind and nobody knows who will be famous tomorrow and what will go viral the day after. An innocent tweet, a [short post on Reddit](https://remoteclan.com/s/27ihu5/my_product_scale_went_viral_150_000_views) or a routine launch on Product Hunt can [shift a new startup from 0 to 10,000 users](https://medium.com/@vinayh/0-10-000-users-how-openvid-launched-on-product-hunt-575ff9ecf7a1) in the span of 3 months. That level of virality is insane. Imagine having 10,000 willing customers on your doorstep and they can't sign in because someone told you that you won't have to think about scale for a good while. Don't become a victim of your own success. ## Do you need want Docker? -Nobody really needs Docker (or containers per se) and I'm not going to claim that containers are the perfect silver bullet solution to all the issues from above, but it remains an incredibly powerful tool which can address many of today's challenges in a very time and cost effective way. Sure there is an upfront investment to be made in learning container technology and putting it into practice, but by no means is it any harder or more time intensive than learning a new CSS framework or the latest flavour of JS. If anything skills like Docker are much more generic and more broadly transferable between tech stacks, programming languages and jobs. +Nobody really needs Docker (or containers per se) and I'm not going to claim that containers are the perfect silver bullet to all the issues listed above, but it remains an incredibly powerful tool which can address many of today's challenges in a very time and cost effective way. Sure there is an upfront investment to be made in learning container technology and putting it into practice, but by no means is it any harder or more time intensive than learning a new CSS framework or the latest flavour of JS. If anything, skills like Docker are much broader applicable and more transferable between programming languages, tech stacks and jobs. -Containers make builds predictable, it makes deployments reliable and it makes horizontal scaling a breeze. Containers are a great way of providing stable and backwards compatible APIs whilst keeping code complexity low. Containers can reduce infrastructure cost by running multiple applications on the same box. They can accelerate a team's productivity by running different feature branches at the same time and scale testing environments independent of hardware. At last containers make blue green deployments easy and help to keep downtime low. +Containers make builds predictable, they make deployments reliable and they make horizontal scaling a breeze. Containers are a great way of providing stable and backwards compatible APIs whilst keeping code complexity low. Containers can reduce infrastructure cost by running multiple applications on the same box. They can accelerate a team's productivity by running different feature branches at the same time and launch testing environments independent of hardware. Containers make blue green deployments mainstream and help to keep downtime low. -Therefore, my question is, do you want Docker? \ No newline at end of file +The question is, why would you not want Docker? \ No newline at end of file