Creating a .NET Core Global Tool
05 Oct 2020
Overview ☀
I have now built my first .NET Core Global Tool!
A .NET Core Global Tool is special NuGet package that contains a console application that is installed globally on your machine.
It is installed in a default directory that is added to the PATH environment variable.
This means you can invoke the tool from any directory on the machine without specifying its location.
The Application 🌱
So, rather than the usual Hello World example to install as a global tool I wanted a tool that would be useful to me.
I wanted to build a tool that will create a folder prefixed with either a bespoke reference (in my case a Trello card number) or the current date in a YYYY-MM-DD format followed by a normal folder name.
The tool once it has created the folder will then also copy some dotfiles that I find useful in most projects over.
For example:
818_create-dotnet-tool
2020-09-29_create-dotnet-tool
It will also copy the following dotfiles over:
- .dockerignore
- .editorconfig
- .gitattributes
- .gitignore
- .prettierignore
- .prettierrc
- omnisharp.json
I won’t explain how this code was written; you can view the source code over at GitHub to understand how this was done.
The important thing to note is that the application is a standard .NET Core console application that you can create as follows:
dotnet new console -n solrevdev.seedfolder
Metadata 📖
What sets a standard .NET Core console application and a global tool apart is some important metadata in the `.csproj` file.
Sdk="Microsoft.NET.Sdk">
Exe
netcoreapp3.1
true
seedfolder
./nupkg
true
1.0.0
solrevdev.seedfolder
A nice description of your tool
A nice description of your tool
your github username
your github username
https://github.com/username/projectname
https://github.com/username/projectname
https://github.com/username/projectname
MIT
git
dotnetcore;;dotnet;csharp;dotnet-global-tool;dotnet-global-tools;
The extra tags from PackAsTool
to Version
are required fields while the Title
to PackageTags
are useful to help describe the package in NuGet and help get it discovered.
Packaging and Installation ⚙
Once I was happy that my console application was working the next step was to create a NuGet package by running the dotnet pack command:
This produces a nupkg package. This nupkg NuGet package is what the .NET Core CLI uses to install the global tool.
So, to package and install locally without publishing to NuGet which will be needed while you are still testing you need the following:
dotnet pack
dotnet tool install --global --add-source ./nupkg solrevdev.seedfolder
Your tool should now be in your path accessible from any folder.
You call your tool whatever was in the ToolCommandName property in your .csproj file
You may find you need uninstall and install while you debug.
To uninstall you need to do as follows:
dotnet tool uninstall -g solrevdev.seedfolder
Once you are happy with your tool and you have installed in globally and tested it you can now publish this to NuGet.
Publish to NuGet 🚀
Head over to NuGet and create an API Key
Once you have this key go to your GitHub Project and under settings and secrets create a new secret named NUGET_API_KEY
with the value you just created over at NuGet.
Finally create a new workflow like the one below which will check out the code, build and package the .NET Core console application as a NuGet package then using the API key we just created we will automatically publish the tool to NuGet.
Each time you commit do not forget to bump the version tag e.g. 1.0.0
name: CI
on:
push:
branches:
- master
- release/*
pull_request:
branches:
- master
- release/*
jobs:
build:
runs-on: windows-latest
steps:
- name: checkout code
uses: actions/checkout@v2
- name: setup .net core sdk
uses: actions/setup-dotnet@v1
with:
dotnet-version: '3.1.x' # SDK Version to use; x will use the latest version of the 3.1 channel
- name: dotnet build
run: dotnet build solrevdev.seedfolder.sln --configuration Release
- name: dotnet pack
run: dotnet pack solrevdev.seedfolder.sln -c Release --no-build --include-source --include-symbols
- name: setup nuget
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
uses: NuGet/setup-nuget@v1.0.2
with:
nuget-version: latest
- name: Publish NuGet
uses: rohith/publish-nuget@v2.1.1
with:
PROJECT_FILE_PATH: src/solrevdev.seedfolder.csproj # Relative to repository root
NUGET_KEY: $ # nuget.org API key
PACKAGE_NAME: solrevdev.seedfolder
Find More 🔍
Now that you have built and published a .NET Core Global Tool you may wish to find some others for inspiration.
Search the NuGet website by using the “.NET tool” package type filter or see the list of tools in the natemcmaster/dotnet-tools GitHub repository.
Success! 🎉
Archiving all bookmarks using the Pocket Developer API
07 Jun 2020
Background
Today I wanted to clean up my Pocket account, I had thousands of unread articles in my inbox
and while their web interface allows you to bulk edit your bookmarks it would have taken days to archive all of them that
way.
So, instead of spending days to do this, I used their API and ran a
quick and dirty script to archive bookmarks going back to 2016!
Here be dragons!
Now, since I ran this script I found a handy dandy page that would have done the job for me although instead of archiving
all my bookmarks it would have deleted them so I am pleased I used my script instead.
If you want to clear your Pocket account without deleting your account head over to this page:
https://getpocket.com/privacy_clear
To be clear this will delete ALL your bookmarks and there is no going back
So, If like me you want to archive all your content carry on reading
Onwards!
To follow along you will need Visual Studio Code and a marketplace plugin called
Rest Client which allows you to interact with
API’s nicely.
I will not be using it to its full potential as it supports variables and such like so I will leave that for an exercise
for the reader to refactor away.
So, to get started create a working folder, 2 files to work with and then open Visual Studio Code
mkdir pocket-api
cd pocket-api
touch api.http
touch api.js
code .
Step 1: Obtain a Pocket platform consumer key
Create a new application over at https://getpocket.com/developer/apps/new
and make sure you select all of the Add/Modify/Retrieve permissions and choose Web as the platform.
Make a note of the consumer_key
that is created.
You can also find it over at https://getpocket.com/developer/apps/
Step 2: Obtain a request token
To begin the Pocket authorization process, our script must obtain a request token from Pocket by making a POST request.
So in api.http
enter the following
### Step 2: Obtain a request token
POST https://getpocket.com/v3/oauth/request HTTP/1.1
Content-Type: application/json; charset=UTF-8
X-Accept: application/json
{
"consumer_key":"11111-1111111111111111111111",
"redirect_uri":"https://solrevdev.com"
}
This redirect_uri does not matter. You can enter anything here.
Using the Rest Client Send Request
feature you can make the request and get the response in the right-hand pane.
You will get a response that gives you a code
that you need for the next step so make sure you make a note of it
{
code:'111111-1111-1111-1111-111111'
}
Step 3: Redirect user to Pocket to continue authorization
Take your code
and redirect_url
from Step 2 above and replace in the URL below and copy and paste the below URL in to a browser and follow the instructions.
https://getpocket.com/auth/authorize?request_token=111111-1111-1111-1111-111111&redirect_uri=https://solrevdev.com
Step 4: Receive the callback from Pocket
Pocket will redirect you to the redirect_url
you entered in step 3 above.
This step authorizes the application giving it the add/modify/delete permissions we asked for in step 1.
Step 5: Convert a request token into a Pocket access token
Now that you have given your application the permissions it needs you can now get an access_token
to make further requests.
Enter the following into api.http
replacing consumer_key
and code
from Steps 1 and 2 above.
POST https://getpocket.com/v3/oauth/authorize HTTP/1.1
Content-Type: application/json; charset=UTF-8
X-Accept: application/json
{
"consumer_key":"11111-1111111111111111111111",
"code":"111111-1111-1111-1111-111111"
}
Again, using the fantastic Rest Client send the request and make a note of the access_token
in the response
{
"access_token": "111111-1111-1111-1111-111111",
"username": "solrevdev"
}
Make some requests
Now we have an access_token
we can make some requests against our account, take a look at the documentation for more information on what can be done with the API
We can view all pockets:
### get all pockets
POST https://getpocket.com/v3/get HTTP/1.1
Content-Type: application/json; charset=UTF-8
X-Accept: application/json
{
"consumer_key":"1111-1111111111111111111111111",
"access_token":"111111-1111-1111-1111-111111",
"count":"100",
"detailType":"simple",
"state": "unread"
}
We can modify pockets:
### modify pockets
POST https://getpocket.com/v3/send HTTP/1.1
Content-Type: application/json; charset=UTF-8
X-Accept: application/json
{
"consumer_key":"1111-1111111111111111111111111",
"access_token":"111111-1111-1111-1111-111111",
"actions" : [
{
"action": "archive",
"item_id": "82500974"
}
]
}
Generate Code Snippet
I used the Generate Code Snippet feature of the Rest Client Extension to get me some
boilerplate code which I extended to loop until I had no more bookmarks left archiving them in batches of 100.
To do this once you’ve sent a request as above, use shortcut Ctrl+Alt+C or Cmd+Alt+C for macOS, or right-click in the editor and then select Generate Code Snippet in the menu, or press F1 and then select/type Rest Client: Generate Code Snippet
.
It will show the available languages.
Select JavaScript
then select enter and your code will appear in a right-hand pane.
Below is that code slightly modified to iterate all unread items then archive them until all complete.
You will need to replace consumer_key
and access_token
for the values you noted earlier.
let keepGoing = true;
while (keepGoing) {
let response = await fetch('https://getpocket.com/v3/get', {
method: 'POST',
headers: {
'content-type': 'application/json; charset=UTF-8',
'x-accept': 'application/json'
},
body:
'{"consumer_key":"1111-1111111111111111111111111","access_token":"111111-1111-1111-1111-111111","count":"100","detailType":"simple","state": "unread"}'
});
let json = await response.json();
//console.log('json', json);
let list = json.list;
//console.log('list', list);
let actions = [];
for (let index = 0; index < Object.keys(list).length; index++) {
let current = Object.keys(list)[index];
let action = {
action: 'archive',
item_id: current
};
actions.push(action);
}
//console.log('actions', actions);
let body =
'{"consumer_key":"1111-1111111111111111111111111","access_token":"111111-1111-1111-1111-111111","actions" : ' +
JSON.stringify(actions) +
'}';
//console.log('body', body);
let response = await fetch('https://getpocket.com/v3/send', {
method: 'POST',
headers: {
'content-type': 'application/json; charset=UTF-8',
'x-accept': 'application/json'
},
body: body
});
let json = await response.json();
console.log('http post json', json);
let status = json.status;
if (status !== 1) {
console.log('done');
keepGoing = false;
} else {
console.log('more items to process');
}
}
Run in Chrome’s console window
And so the quick and dirty solution for me was to copy the above JavaScript and in a Chrome console window paste and run.
It took a while as I had content going back to 2016 but once it was finished I had a nice clean inbox again!
Success 🎉
Adding TypeScript to an existing aspnetcore project
06 Jun 2020
Background
So, I have a small ASP.NET Core Razor Pages application that I recently enhanced by adding Vue in the same way that I once would add jQuery to an existing application to add some interactivity to an existing page.
Not all websites need to be SPA’s with full-on JavaScript frameworks and build processes and just like with jQuery back in the day I was able to add Vue by simply adding a
tag to my page.
include="Development">
exclude="Development">
The one issue I did have was that my accompanying code used the latest and greatest JavaScript features which ruled out the page working on some older browsers.
This needed fixing!
TypeScript to the rescue
One of the reasons I prefer Vue over React and other JavaScript frameworks is that it’s so easy to simply add Vue to an existing project without going all in.
You can add as little or as much as you want.
TypeScript I believe is similar in that you can add it bit by bit to a project.
And not only do you get type safety as a benefit but it can also transpile TypeScript to older versions of JavaScript.
Exactly what I wanted!
So for anyone else that wants to do the same and for future me wanting to know how to do this here we are!
Install TypeScript NuGet package
First you need to install the Microsoft.TypeScript.MSBuild nuget package into your ASP.NET Core website project.
This will allow you to build and transpile from your IDE, the command line or even a build server.
Create tsconfig.json
Next up create a tsconfig.json
file in the root of your website project. This tells the TypeScript compiler what to do and how to behave.
{
"compilerOptions": {
"lib": ["DOM", "ES2015"],
"target": "es5",
"noEmitOnError": true,
"strict": false,
"module": "es2015",
"moduleResolution": "node",
"outDir": "wwwroot/js"
},
"include": ["Scripts/**/*"],
"compileOnSave": true
}
- target : The target is es5 which is the JavaScript version I want to support and transpile down to.
- noEmitOnError: This will stop the script wiping any existing code if the TypeScript errors.
- outDir: I want the source TypeScript to put the JavaScript in the same place I was putting my original code
- include: This says take all the TypeScript in this folder and transpile into .js files of the same name into outDir above
- compileOnSave: This is a productivity booster!
Create Folders
Now create a Scripts
folder alongside Pages
to store the TypeScript files.
Create first TypeScript file
Add the following to Scripts/site.ts
and then save the file to kick off the TypeScript compiler.
export {};
if (window.console) {
let message: string = 'site.ts > site.js > site.js.min';
console.log(message);
}
Save And Build!
If all has gone well there should be a site.js
file in the wwwroot\js
folder.
Now whenever the project is built every .ts
file you add to Scripts
will be transpiled to a file with the same name but with a .js
extension in the wwwroot\js
folder.
And best of all you should notice that it has taken the let
keyword in the source TypeScript file and transpiled that to var
in the destination site.js JavaScript file.
Before
export {};
if (window.console) {
let message: string = 'site.ts > site.js > site.js.min';
console.log(message);
}
After
if (window.console) {
var message = 'site.ts > site.js > site.js.min';
console.log(message);
}
TypeScript with Vue, jQuery and Lodash
However, while site.js is a nice simple example, my project as I mentioned above uses Vue (and jQuery and Lodash) and if you try and build that with TypeScript you may get errors related to those external libraries.
One fix would be to import the types for those libraries however, I wanted to keep my project simple and do not want to try and import types for my external libraries.
So, the following example shows how to tell TypeScript that your code is using Vue, jQuery and Lodash while keeping the codebase light and not having to import any types.
You will not get full intellisense for these as TypeScript does not have the type definitions for them however you will not get any errors because of them.
That for me was fine.
export { };
declare var Vue: any;
declare var _: any;
declare var $: any;
const app = new Vue({
el: '#app',
data: {
message: 'Hello Vue!'
},
created: function () {
const form = document.getElementById('form') as HTMLFormElement;
const email = (document.getElementById('email') as HTMLInputElement).value;
const button = document.getElementById('submit') as HTMLInputElement;
},
});
$(document).ready(function () {
setTimeout(function () {
$(".jqueryExample").fadeTo(1000, 0).slideUp(1000, function () {
$(this).remove();
});
}, 10000);
});
Another common error is that TypeScript may not know about HTML form elements.
As in the example above you can fix this by declaring your form variables as the relevant types.
In my case the common ones were HTMLFormElement
and HTMLInputElement
.
And that is it basically!
More TypeScript?
So, for now, this is the right amount of TypeScript for my needs.
I did not have to bring too much ceremony to my application but I still get some type checking and more importantly I can code using the latest language features but still have JavaScript that works in older browsers.
If the project grows I will see how else I can improve it with TypeScript!
Success 🎉
Instagram Basic Display API
28 May 2020
Background
A while ago I was working on a project that consumed the Instagram Legacy API Platform.
To make things easier there was a fantastic library called InstaSharp which wrapped the HTTP calls to the Instagram Legacy API endpoints.
However, Instagram began disabling the Instagram Legacy API Platform and on June 29, 2020, any remaining endpoints will no longer be available.
The replacements to the Instagram Legacy API Platform are the Instagram Graph API and the Instagram Basic Display API.
So, If my project was to continue to work I needed to migrate over to the Instagram Basic Display API before the deadline.
I decided to build and release an open-source library, A wrapper around the Instagram Basic Display API in the same way as InstaSharp did for the original.
Solrevdev.InstagramBasicDisplay
And so began Solrevdev.InstagramBasicDisplay, a netstandard2.0 library that consumes the new Instagram Basic Display API.
It is also available on nuget so you can add this functionality to your .NET projects.
Getting Started
So, to consume the Instagram Basic Display API you will need to generate an Instagram client_id
and client_secret
by creating a Facebook app and configuring it so that it knows your https only redirect_url
.
Facebook and Instagram Setup
Before you begin you will need to create an Instagram client_id
and client_secret
by creating a Facebook app and configuring it so that it knows your redirect_url
. There are full instructions here.
Step 1 - Create a Facebook App
Go to developers.facebook.com, click My Apps, and create a new app. Once you have created the app and are in the App Dashboard, navigate to Settings > Basic, scroll the bottom of page, and click Add Platform.
Choose Website, add your website’s URL, and save your changes. You can change the platform later if you wish, but for this tutorial, use Website
Step 2 - Configure Instagram Basic Display
Click Products, locate the Instagram product, and click Set Up to add it to your app.
Click Basic Display, scroll to the bottom of the page, then click Create New App.
In the form that appears, complete each section using the guidelines below.
Display Name
Enter the name of the Facebook app you just created.
Valid OAuth Redirect URIs
Enter https://localhost:5001/auth/oauth/ for your redirect_url
that will be used later. HTTPS must be used on all redirect URLs
Deauthorize Callback URL
Enter https://localhost:5001/deauthorize
Data Deletion Request Callback URL
Enter https://localhost:5001/datadeletion
App Review
Skip this section for now since this is just a demo.
Step 3 - Add an Instagram Test User
Navigate to Roles > Roles and scroll down to the Instagram Testers section. Click Add Instagram Testers and enter your Instagram account’s username and send the invitation.
Open a new web browser and go to www.instagram.com and sign in to your Instagram account that you just invited. Navigate to (Profile Icon) > Edit Profile > Apps and Websites > Tester Invites and accept the invitation.
You can view these invitations and applications by navigating to (Profile Icon) > Edit Profile > Apps and Websites
Facebook and Instagram Credentials
Navigate to My Apps > Your App Name > Basic Display
Make a note of the following Facebook and Instagram credentials:
Instagram App ID
This is going to be known as client_id
later
Instagram App Secret
This is going to be known as client_secret
later
Client OAuth Settings > Valid OAuth Redirect URIs
This is going to be known as redirect_url
later
go here for a full size screenshot
Installation
Now that you have an Instagram client_id
and client_secret
to use we can now create a new dotnet project and add the Solrevdev.InstagramBasicDisplay package to it.
Create a .NET Core Razor Pages project.
dotnet new webapp -n web
cd web
To install via nuget using the dotnet cli
dotnet add package Solrevdev.InstagramBasicDisplay
To install via nuget using Visual Studio / Powershell
Install-Package Solrevdev.InstagramBasicDisplay
App Configuration
In your .NET Core library or application create an appsettings.json
file if one does not already exist and fill out the InstagramSettings
section with your Instagram credentials such as client_id, client_secret and redirect_url as mentioned above.
appsettings.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"InstagramCredentials": {
"Name": "friendly name or your app name can go here - this is passed to Instagram as the user-agent",
"ClientId": "client-id",
"ClientSecret": "client-secret",
"RedirectUrl": "https://localhost:5001/auth/oauth"
}
}
Common Uses
Now that you have a .NET Core Razor Pages website and the Solrevdev.InstagramBasicDisplay library has been added you can achieve some of the following common uses.
Get an Instagram User Access Token and permissions from an Instagram user
First, you send the user to Instagram to authenticate using the Authorize
method, they will be redirected to the RedirectUrl
set in InstagramCredentials
so ensure that is set-up correctly in the Instagram app settings page.
Instagram will redirect the user on successful login to the RedirectUrl
page you configured in InstagramCredentials
and this is where you can call AuthenticateAsync
which exchanges the Authorization Code for a short-lived Instagram user access token or optionally a long-lived Instagram user access token.
You then have access to an OAuthResponse
which contains your access token and a user which can be used to make further API calls.
private readonly InstagramApi _api;
public IndexModel(InstagramApi api)
{
_api = api;
}
public ActionResult OnGet()
{
var url = _api.Authorize("anything-passed-here-will-be-returned-as-state-variable");
return Redirect(url);
}
Then in your RedirectUrl
page
private readonly InstagramApi _api;
private readonly ILogger<IndexModel> _logger;
public IndexModel(InstagramApi api, ILogger<IndexModel> logger)
{
_api = api;
_logger = logger;
}
// code is passed by Instagram, the state is whatever you passed in _api.Authorize sent back to you
public async Task<IActionResult> OnGetAsync(string code, string state)
{
// this returns an access token that will last for 1 hour - short-lived access token
var response = await _api.AuthenticateAsync(code, state).ConfigureAwait(false);
// this returns an access token that will last for 60 days - long-lived access token
// var response = await _api.AuthenticateAsync(code, state, true).ConfigureAwait(false);
// store in session - see System.Text.Json code below for sample
HttpContext.Session.Set("Instagram.Response", response);
}
If you want to store the OAuthResponse
in HttpContext.Session
you can use the new System.Text.Json
namespace like this
using System.Text.Json;
using Microsoft.AspNetCore.Http;
public static class SessionExtensions
{
public static void Set<T>(this ISession session, string key, T value)
{
session.SetString(key, JsonSerializer.Serialize(value));
}
public static T Get<T>(this ISession session, string key)
{
var value = session.GetString(key);
return value == null ? default : JsonSerializer.Deserialize<T>(value);
}
}
Get an Instagram user’s profile
private readonly InstagramApi _api;
private readonly ILogger<IndexModel> _logger;
public IndexModel(InstagramApi api, ILogger<IndexModel> logger)
{
_api = api;
_logger = logger;
}
// code is passed by Instagram, the state is whatever you passed in _api.Authorize sent back to you
public async Task<IActionResult> OnGetAsync(string code, string state)
{
// this returns an access token that will last for 1 hour - short-lived access token
var response = await _api.AuthenticateAsync(code, state).ConfigureAwait(false);
// this returns an access token that will last for 60 days - long-lived access token
// var response = await _api.AuthenticateAsync(code, state, true).ConfigureAwait(false);
// store and log
var user = response.User;
var token = response.AccessToken;
_logger.LogInformation("UserId: {userid} Username: {username} Media Count: {count} Account Type: {type}", user.Id, user.Username, user.MediaCount, user.AccountType);
_logger.LogInformation("Access Token: {token}", token);
}
Get an Instagram user’s images, videos, and albums
private readonly InstagramApi _api;
private readonly ILogger<IndexModel> _logger;
public List<Media> Media { get; } = new List<Media>();
public IndexModel(InstagramApi api, ILogger<IndexModel> logger)
{
_api = api;
_logger = logger;
}
// code is passed by Instagram, the state is whatever you passed in _api.Authorize sent back to you
public async Task<IActionResult> OnGetAsync(string code, string state)
{
// this returns an access token that will last for 1 hour - short-lived access token
var response = await _api.AuthenticateAsync(code, state).ConfigureAwait(false);
// this returns an access token that will last for 60 days - long-lived access token
// var response = await _api.AuthenticateAsync(code, state, true).ConfigureAwait(false);
// store and log
var media = await _api.GetMediaListAsync(response).ConfigureAwait(false);
_logger.LogInformation("Initial media response returned with [{count}] records ", media.Data.Count);
_logger.LogInformation("First caption: {caption}, First media url: {url}",media.Data[0].Caption, media.Data[0].MediaUrl);
//
// toggle the following boolean for a quick and dirty way of getting all a user's media.
//
if(false)
{
while (!string.IsNullOrWhiteSpace(media?.Paging?.Next))
{
var next = media?.Paging?.Next;
var count = media?.Data?.Count;
_logger.LogInformation("Getting next page [{next}]", next);
media = await _api.GetMediaListAsync(next).ConfigureAwait(false);
_logger.LogInformation("next media response returned with [{count}] records ", count);
// add to list
Media.Add(media);
}
_logger.LogInformation("The user has a total of {count} items in their Instagram feed", Media.Count);
}
}
Exchange a short-lived access token for a long-lived access token
private readonly InstagramApi _api;
private readonly ILogger<IndexModel> _logger;
public IndexModel(InstagramApi api, ILogger<IndexModel> logger)
{
_api = api;
_logger = logger;
}
// code is passed by Instagram, the state is whatever you passed in _api.Authorize sent back to you
public async Task<IActionResult> OnGetAsync(string code, string state)
{
// this returns an access token that will last for 1 hour - short-lived access token
var response = await _api.AuthenticateAsync(code, state).ConfigureAwait(false);
_logger.LogInformation("response access token {token}", response.AccessToken);
var longLived = await _api.GetLongLivedAccessTokenAsync(response).ConfigureAwait(false);
_logger.LogInformation("longLived access token {token}", longLived.AccessToken);
}
Refresh a long-lived access token for another long-lived access token
private readonly InstagramApi _api;
private readonly ILogger<IndexModel> _logger;
public IndexModel(InstagramApi api, ILogger<IndexModel> logger)
{
_api = api;
_logger = logger;
}
// code is passed by Instagram, the state is whatever you passed in _api.Authorize sent back to you
public async Task<IActionResult> OnGetAsync(string code, string state)
{
// this returns an access token that will last for 1 hour - short-lived access token
var response = await _api.AuthenticateAsync(code, state).ConfigureAwait(false);
_logger.LogInformation("response access token {token}", response.AccessToken);
var longLived = await _api.GetLongLivedAccessTokenAsync(response).ConfigureAwait(false);
_logger.LogInformation("longLived access token {token}", longLived.AccessToken);
var another = await _api.RefreshLongLivedAccessToken(response).ConfigureAwait(false);
_logger.LogInformation("response access token {token}", another.AccessToken);
}
Sample Code
For more documentation and a sample ASP.Net Core Razor Pages web application visit the samples folder in the GitHub repo
Success 🎉
Deploy ASP.NET Core Web API to Fly via Docker
18 May 2020
In my last post I deployed the standard Blazor template over to vercel static site hosting.
In the standard template, the FetchData
component gets its data from a local sample-data/weather.json
file via an HttpClient
.
forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("sample-data/weather.json");
I wanted to upgrade this by replacing that call to the local json file with a call to an ASP.NET Core Web API backend.
Unfortunately unlike in the version 1 days of zeit where you could deploy Docker based apps to them vercel now offer serverless functions instead but do not support .NET.
So, as an alternative, I looked at fly.io.
I first used them in 2017 before GitHub supported HTTPS/SSL for custom domains by using them as middleware to provide this service.
Since then they now support deploying Docker based app servers which works in pretty much the same way as zeit used to.
Perfect!
Backend
So, the plan was to create a backend to replace the weather.json file, deploy and host it via Docker on fly.io and point my vercel hosted blazor website to that!
First up I created a backend web API using the dotnet new
template and added that to my solution.
Fortunately, the .NET Core Web API template comes out of the box with a /weatherforecast
endpoint that returns the same shape data as the sample_data/weather.json
file in the frontend.
dotnet new webapi -n backend
dotnet sln add backend/backend.csproj
Next, I needed to tell my web API backend that another domain (my vercel hosted blazor app) would be connecting to it. This would fix any CORS related error messages.
So in backend/Program.cs
private readonly string _myAllowSpecificOrigins = "_myAllowSpecificOrigins";
public void ConfigureServices(IServiceCollection services)
{
services.AddCors(options =>
{
options.AddPolicy(name: _myAllowSpecificOrigins,
builder =>
{
builder.WithOrigins("https://blazor.now.sh",
"https://blazor.solrevdev.now.sh",
"https://localhost:5001",
"http://localhost:5000");
});
});
services.AddControllers();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseCors(policy =>
policy
.WithOrigins("https://blazor.now.sh",
"https://blazor.solrevdev.now.sh",
"https://localhost:5001",
"http://localhost:5000")
.AllowAnyMethod()
.WithHeaders(HeaderNames.ContentType));
app.UseAuthorization();
app.UseEndpoints(endpoints => endpoints.MapControllers());
}
Docker
Now that the backend project is ready it was time to deploy it to https://fly.io/.
From a previous project, I already had a handy dandy working Dockerfile I could re-use so making sure I replaced the name of dotnet dll and ensured I was pulling a recent version of .NET Core SDK
Dockerfile
FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS build
# update the debian based system
RUN apt-get update && apt-get upgrade -y
# install my dev dependacies inc sqllite and curl and unzip
RUN apt-get install -y sqlite3
RUN apt-get install -y libsqlite3-dev
RUN apt-get install -y curl
RUN apt-get install -y unzip
# not sure why im deleting these
RUN rm -rf /var/lib/apt/lists/*
# add debugging in a docker tooling - install the dependencies for Visual Studio Remote Debugger
RUN apt-get update && apt-get install -y --no-install-recommends unzip procps
# install Visual Studio Remote Debugger
RUN curl -sSL https://aka.ms/getvsdbgsh | bash /dev/stdin -v latest -l ~/vsdbg
WORKDIR /app/web
# layer and build
COPY . .
WORKDIR /app/web
RUN dotnet restore
# layer adding linker then publish after tree shaking
FROM build AS publish
WORKDIR /app/web
RUN dotnet publish -c Release -o out
# final layer using smallest runtime available
FROM mcr.microsoft.com/dotnet/core/aspnet:3.1 AS runtime
WORKDIR /app/web
COPY --from=publish app/web/out ./
# expose port and execute aspnetcore app
EXPOSE 5000
ENV ASPNETCORE_URLS=http://+:5000
ENTRYPOINT ["dotnet", "backend.dll"]
The lines of code in that Dockerfile that were really important for fly.io to work were
EXPOSE 5000
ENV ASPNETCORE_URLS=http://+:5000
I also created a .dockerignorefile
I had already installed and authenticated the flyctl
command-line tool, head over to https://fly.io/docs/speedrun/ for a simple tutorial on how to get started.
After some trial and error and some fantastic help from support, I worked out that I needed to override the port that fly.io used so that it matched my .NET Core Web API project.
I created an app using port 5000 by first navigating into the backend project so that I was in the same location as the csproj file.
cd backend
flyctl apps create -p 5000
You should find a new fly.toml
file has been added to your project folder
app = "blue-dust-2805"
[[services]]
internal_port = 5000
protocol = "tcp"
[services.concurrency]
hard_limit = 25
soft_limit = 20
[[services.ports]]
handlers = ["http"]
port = "80"
[[services.ports]]
handlers = ["tls", "http"]
port = "443"
[[services.tcp_checks]]
interval = 10000
timeout = 2000
Make a mental note of the app name you will see it again in the final hostname, also note the port number that we overrode in the previous step.
Now to deploy the app…
And get the deployed endpoint URL back to use in the front end…
The flyctl info
command will return a deployed endpoint along with a random hostname such as
flyctl info
App
Name = blue-dust-2805
Owner = your fly username
Version = 10
Status = running
Hostname = blue-dust-2805.fly.dev
Services
PROTOCOL PORTS
TCP 80 => 5000 [HTTP]
443 => 5000 [TLS, HTTP]
IP Addresses
TYPE ADDRESS CREATED AT
v4 77.83.141.66 2020-05-17T20:49:30Z
v6 2a09:8280:1:c3b:5352:d1d5:9afd:fb65 2020-05-17T20:49:31Z
Now that the app is deployed you can view it by taking the hostname blue-dust-2805.fly.dev
and appending the weather forecast endpoint at the end.
For example https://blue-dust-2805.fly.dev/weatherforecast
If all has gone well you should see some random weather!
Login to you fly.io control panel to see some stats
Frontend
Next up it was just a case of replacing the frontend’s call to the local json file with the backend endpoint.
builder.Services.AddTransient(sp => new HttpClient { BaseAddress = new Uri("https://blue-dust-2805.fly.dev") });
A small change to the FetchData.razor
page.
protected override async Task OnInitializedAsync()
{
_forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("weatherforecast");
}
Re-deploy that to vercel by navigating to the root of our solution and running the deploy.sh script or manually via
cd ../../
dotnet publish -c Release
now --prod frontend/bin/Release/netstandard2.1/publish/wwwroot/
Test that everything has worked by navigating to the FetchData
endpoint of our frontend. In my case https://blazor.now.sh/fetchdata
GitHub Actions
As a final nice to have fly.io have GitHub action we can use to automatically build and deploy our Dockerfile based .NET Core Web API on each push or pull request to GitHub.
Create an auth token in your project
cd backend
flyctl auth token
Go to your repository on GitHub and select Setting then to Secrets and create a secret called FLY_API_TOKEN with the value of the token we just created.
Next, create the file .github/workflows/fly.yml
name: Fly Deploy
on:
push:
branches:
- master
- release/*
pull_request:
branches:
- master
- release/*
env:
FLY_API_TOKEN: $
FLY_PROJECT_PATH: backend
jobs:
deploy:
name: Deploy app
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: superfly/flyctl-actions@1.0
with:
args: "deploy"
Notice that in that file we have told the GitHub action to use the FLY_API_TOKEN
we just setup.
Also because my fly.toml
is not in the solution root but in the backend folder I can tell fly to look for it by setting the environment variable FLY_PROJECT_PATH
FLY_API_TOKEN: $
FLY_PROJECT_PATH: backend
Also, make sure the fly.toml
is not in your .gitignore file.
And so with that, every time I accept a pull request or I push to master my backend will get deployed to fly.io!
The new code is up on GitHub at https://github.com/solrevdev/blazor-on-vercel
Success 🎉