Download as pdf or txt
Download as pdf or txt
You are on page 1of 106

Sold to

icoronel@humanfactor.mx
Table of Contents
Introduction 1.1
Chapter 1: Setting up and testing your Shopify app. 1.2
Chapter 2: Setting up and using "user secrets" 1.3
Chapter 3: Dynamic application URLs for development and production 1.4
Chapter 4: User accounts and Entity Framework 1.5
Chapter 5: Startup and Dependency Injection 1.6
Chapter 6: Authentication extensions and validation attributes 1.7
Chapter 7: Handling Shopify's OAuth installation/login process 1.8
Chapter 8: Signing users up for a monthly subscription plan 1.9
Chapter 9: Using the Shopify API to load a list of Shopify orders 1.10
Chapter 10: Validating and handling Shopify's webhooks 1.11
Chapter 11: Taking AuntieDot for a test drive 1.12

2
Introduction

Introduction.
Building an app for the Shopify store is hard. Really hard. There are tons of API calls, redirection
URLs, webhooks and best practices that you'll need to stitch together just to get something that
works half of the time. There's not one single place, website or resource that will show you all of
the steps you'll need to take to build a rock-solid Shopify app. Almost all of those resources will
use NodeJS or Ruby on Rails -- which can sometimes feel like you're reading hieroglyphs if
you're a C# and ASP.NET developer.

Shopify's own partner blog puts a heavy focus on designing Shopify themes, rather than building
real, functional apps. Their API docs are barebones, don't document all available properties on
objects, and often leave you yearning for more (especially if you don't know what you're looking
for).

If you've ever tried to build a Shopify app, you've probably asked yourself these questions (heck,
I asked them myself when I first started out):

How can I charge my users when they use my app?


What in the world is an embedded app?
What do I need to do to get my app to load inside the Shopify admin dashboard?
How should I be using Shopify's redirect URLs?
When should I be using a proxy page?
Am I dealing with webhooks the right way?
How can I let my user's actual customers interact with the app?
Can I add custom scripts to their website, and what can those scripts even do?
How the heck do I go about testing my app?
What's the best method for validating that requests from Shopify are actually from Shopify?

That's why I wrote The Shopify Development Handbook -- to distill my own experience of
building Shopify applications into one concise and comprehensive course for ASP.NET
developers.

In this guide, we'll go much further and deeper than Shopify's own documentation. We're going
to build up a basic Shopify application, which we'll call AuntieDot, that will connect to a user's
store and sign them up for a monthly subscription to your app. Then, we're going to use that basic
app as the basis for three more advanced apps that you'll be able to deploy to the Shopify app
store.

3
Introduction

All in all, by the time you're done with this course, you'll know everything there is to know about
integrating with a user's store; charging users on a single or recurring basis; pulling in all sorts of
data from the user's store (e.g. orders, customers, transactions etc.); handling and responding to
webhook events; embedding custom widgets or javascript tags on a user's store; and a whole lot
more.

About the author.


One of the most important things you should ask yourself, before spending time on a book like
this, is "why should I listen to this person"? Does he just talk the talk, or can he walk the walk?

My name's Joshua Harms, and I've been a C#, F# and .NET for more than eight years; I've been a
Shopify consultant and app developer for over half of that. Not only have I been consulting
professionally for the last five years, but I've built and run my own Shopify application called
Stages. I'm also the guy behind ShopifySharp, the most comprehensive .NET library for the
Shopify API.

When I'm not consulting, toiling on my own app or working on ShopifySharp, I spend my spare
time writing articles, guides and tutorials that teach other ASP.NET developers to build reliable
Shopify applications for the app store.

A quick thank-you and a note on updates to this


book
Since you purchased this handbook (by the way, thank you!) you'll now receive free updates to it,
for life. And speaking of updates, you'll find that some of the images in this book are a little bit
out of date as Shopify frequently redesigns or changes their partner and admin dashboards.

Not satisfied with just changing the layout or colors in their dashboards, Shopify also makes
frequent changes to their APIs; this means that eventually, things will become out of date and the
API calls you read in this book will eventually stop working. Right now, Shopify's API is on a
rolling release, where a new version is introduced twice every year, and a version from the
previous year is deprecated.

Beyond updating the book as new API versions are released, I also publish fixes for small
mistakes or typos, along with adding new chapters based on feedback and questions from the
readers. If you've got a suggestion for this book about a topic that wasn't clear or wasn't included,
please send me an email at joshua@nozzlegear.com!

4
Introduction

What you'll need


Before we get started, I should warn you: this guide is for nerds only. If you're not a developer,
this is probably not going to make much sense to you. If you are a developer, you should at least
be familiar enough with C# and ASP.NET to write a simple "Hello World" website using the
Model-View-Controller (MVC) framework.

Here's what you're going to need:

1. Visual Studio Code, which is a free download from Microsoft. You could also use JetBrains
Rider, which is what I use primarily on my own machine, but it does have a license fee. If
you're on Windows, you could use the full Visual Studio as well. The choice is up to you,
but if you're not sure which to choose I would strongly recommend sticking with the first
recommendation, Visual Studio Code.
2. A bash terminal of your choice, or PowerShell if you're on Windows. (The new WSL bash
subsystem in Windows will install dotnet packages for Ubuntu, but VSCode is running in
Windows; that causes all sorts of intellisense problems unless you set VSCode up to use the
WSL remoting feature, but that's beyond the scope of this book.)
3. The .NET Core SDK, another free download. For this guide I'm using version 3.1 of the
.NET SDK, but anything above version 3.0 should work with minimal or zero changes.
Once installed you should be able to type dotnet --version in your terminal and get a
message back telling you which version you have installed.
4. A SQL Server 2019 installation. You can either get this from Microsoft's website (the free
developer tier will work just fine), or you can install it via Docker container if you have
Docker installed.
5. A Shopify developer account, which is also free.
6. A localhost forwarder like Ngrok, which makes localhost URLs on your development
machine accessible from the internet. This is vital for testing Shopify webhooks, but is not
strictly necessary for completing the projects in this book. We'll touch more on localhost
forwarding in the next chapter.

If you're using Docker to get your SQL database, here's a quick install script that will open
up the ports and agree to the EULA (which you should read before running this command) -
- note the password and username, you'll need it later: docker run -d -it -p 1433:1433 -
-name shopify-sql-database -e ACCEPT_EULA="Y" -e SA_PASSWORD="a-BAD_passw0rd"
mcr.microsoft.com/mssql/server:2019-latest

Let's get started!

5
Chapter 1: Setting up and testing your Shopify app.

Setting up and testing your Shopify app.


It's time to start building our baseline app, which I'm going to lovingly name "AuntieDot". It's
going to serve as the springboard from which we build three much more advanced Shopify
applications throughout the rest of this guide. Because AuntieDot is the baseline, it's just going to
do six things that all three of the advanced apps are going to need:

1. Let potential users sign up for a new account.


2. Connect each user's Shopify store to your app, after which you'll be able to make API calls
against their store.
3. Sign each user up for a monthly subscription to your service.
4. Prevent users that haven't connected their store, or haven't accepted your subscription
charge, from accessing protected areas of the app.
5. Listen for the "AppUninstalled" webhook, which tells us when a user has uninstalled your
Shopify app.
6. Listen for the GDPR webhooks, which Shopify uses to inform applications that shop or
customer data must be deleted according to their privacy guidelines.

The process of setting up and building AuntieDot will be broken up over this and the next four
chapters.

Once you've got the .NET Core SDK and Visual Studio Code (or your preferred editor) installed,
the very first step we'll take is setting up a new ASP.NET web project using your terminal. So if
you're on Window, open up that Powershell prompt; if you're on macOS, you'll want Terminal;
and if you're on Linux, you already know what to do.

We're going to use the dotnet command provided by the .NET Core SDK to scaffold the
project, so once you've got your terminal open, type the following commands to navigate to your
home directory and create a new project:

Terminal

# Navigate to your home folder (or wherever you want to create the new project)
$ cd ~/

# Create a new folder named shopify-tutorial


$ mkdir shopify-tutorial
$ cd shopify-tutorial

6
Chapter 1: Setting up and testing your Shopify app.

# Create a new ASP.NET web project named "AuntieDot"


$ dotnet new mvc --language c# --name AuntieDot

Note: omit the $ character whenever you see terminal code. This just marks the start of a
new line/command. The lines starting with a # are comments and the entire line can be
omitted.

If it's the first time you've run that dotnet command on your machine, you might see a message
about it filling up a package cache. This usually only takes between a couple seconds and one
minute. Just stick it out, all future dotnet commands will complete in a matter of seconds or
milliseconds.

Once you've run the commands above in your terminal, the dotnet CLI tool will create a bunch of
folders and files, everything you need to start up a basic "Hello World" website using ASP.NET
Core MVC. However, there are a few extra packages we'll need to install that don't come with the
default project. Three of them are for managing data in your SQL database, and the other one is
for making calls to the Shopify API.

In your terminal, run the following package installation commands:

Terminal

dotnet add package Microsoft.EntityFrameworkCore --version 3.1.2


dotnet add package Microsoft.EntityFrameworkCore.Design --version 3.1.2
dotnet add package Microsoft.EntityFrameworkCore.SqlServer --version 3.1.2
dotnet add package ShopifySharp --version 5.6.0
dotnet restre

While you could technically omit the versions and let the dotnet CLI tool grab the latest versions
of those packages, I'd recommend using the ones specified above so that your project matches the
one we're building in this guide as closely as possible. It's not unlikely that by the you read this
guide, there have been newer versions of those packages published, which could change the way
they're used and the way they work.

After the project has been created and the packages have been installed, you can start it up with
the dotnet run command and see what the default new project looks like:

Terminal

$ dotnet run

You'll see a message in your terminal telling you which address the web app is listening on. In
almost all cases that will be http://localhost:5000 , which you can open in your browser.

7
Chapter 1: Setting up and testing your Shopify app.

When you open the URL in your browser, you should see the default ASP.NET Core MVC
website:

Looking good! Copy that localhost URL and hold on to it, because we'll need it in just a few
moments.

8
Chapter 1: Setting up and testing your Shopify app.

Setting up tools for testing your Shopify app.


A huge part of developing any app or website is testing it, and the same goes for a Shopify app.
With all of the code, moving parts and API calls, it's hugely important to make sure that things
actually work the way that you intend them to work.

In many of the chapters and projects throughout the rest of this book, we'll be running the apps
we create and walking through them. We want to both make sure that they're working, and get a
hands-on understanding for how certain things work.

However, when I say "testing your app", I'm not talking about unit tests. While it's perfectly
possible (and, frankly, encouraged) to set up a unit test suite for your Shopify app, there's nothing
special that you'll need to do for that. Running unit tests on a Shopify app works the same way
that you would run unit tests on any other website, app or library.

Instead, when I say "testing your app", I mean actually running it and using it manually while
developing it.

Big deal, right? Just type dotnet run in your terminal, open the website in your browser and
away you go. Unfortunately, it's not quite that simple when it comes to Shopify apps. While it's
possible to run your app and test most of it from localhost, some (very important) Shopify
features require it to run at an address reachable from the public web.

Since localhost can only be reached by you, and only on your device or computer, it doesn't quite
qualify as a "web-reachable" address. If your app is running on localhost, you won't be able to
handle and receive Shopify webhooks; you won't be able to use Shopify proxy pages; you'll have
some difficulties when your app is loaded as an Embedded Shopify App; and you won't be able to
load any script tags on a storefront.

If you want to use webhooks, proxy pages, embedded apps or script tags, you'll need to make
your website reachable on the web. There's two ways you can do this:

1. Buy a custom domain, and then upload your app to your host whenever you want to test one
of those features.
2. Use a localhost forwarder.

While you'll need to get a custom domain, SSL certificate and an app host before you deploy
your app to the Shopify store, it can be extremely tedious to upload and deploy your app to the
host every time you make a change during development. Instead, I'm a big fan of using a
localhost forwarder.

9
Chapter 1: Setting up and testing your Shopify app.

Here's how they work: you download a piece of software, then start your app running on
localhost. Once you've got your localhost URL (e.g. localhost:5000 from the terminal), you
paste it into the forwarder. It'll return a real web address such as random-url.forwarder.com ,
and any web request to that URL will be instantly forwarded to your app running on localhost.

It's a super handy way to access your app from a different computer, a different network, or even
your mobile phone. And, because these forwarders give you a real URL that can be accessed
from the web, they'll work with Shopify webhooks, proxy pages, embedded apps and script tags.

Personally, my preferred service for forwarding localhost and testing my Shopify apps is Ngrok.
They offer a free plan, though it's somewhat limited — it only gives you a randomized URL each
time you start the Ngrok service.

Here's why that's important: when we set up your Shopify app's "manifest" in the next chapter,
you'll need to enter a few URLs. These URLs tell Shopify where to send a user when they try to
install your app, and where they can be sent back to after they accept the installation. They can
only be redirected to URLs on this list.

It's a huge pain in the butt to log in to your Shopify dashboard and edit your app's settings to use
the latest random URL, every time you want to test your app. Making matters more annoying,
you'll need to edit your app's code to change the URLs there, too.

While it's entirely possible to test and use your app on Ngrok's free plan (or on localhost if you
can sacrifice testing some features), I recommend springing for the "premium" $5/month plan
(though it's charged yearly) or the $10/month plan (which is actually charged monthly). With one
of those plans you'll get to pick a permanent, custom URL that you can plug into your app's
settings and never have to change until you launch your app publicly on the Shopify app store.

Disclaimer: I'm not receiving anything for recommending Ngrok. They probably don't even
know about this handbook.

If you do decide to use Ngrok to test your Shopify app throughout the rest of this course, here's
how you set it up:

1. Optionally sign up for a paid Ngrok plan that gives you custom subdomains.
2. Head over to their website and download the Ngrok executable from
https://ngrok.com/download.
3. Unzip the folder, extract the ngrok.exe file to wherever you're comfortable with and copy
the folder path.
4. Add that folder path to your system's Path environment variable. If you've worked with the
command line before you're probably familiar with this, but if not I've recorded this short
YouTube video to show you how to do that on Windows: https://www.youtube.com/watch?

10
Chapter 1: Setting up and testing your Shopify app.

v=hcr7hytSPiQ
5. Open your terminal and run the Ngrok executable with ngrok --help . If you see output
from Ngrok telling you how to use it then you're successful so far. If not you may want to
double-check that your Path environment variable is correct, then close and re-open the
terminal to refresh it.

If you did sign up for a paid Ngrok plan that gives you custom domains, do this next:

1. Find your Ngrok auth token at https://dashboard.ngrok.com/get-started


2. Authenticate Ngrok in your terminal with ngrok authtoken auth-token-from-previous-
step

3. Reserve a custom subdomain at https://dashboard.ngrok.com/reserved by entering a


subdomain name and pressing the Reserve button.
Be careful that you don't enter a full domain like example.com , you just want the
subdomain name. For example, entering auntiedot reserves auntiedot.ngrok.io
for me.
4. Start Ngrok on your localhost port using your reserved subdomain. If your localhost URL is
localhost:5000 then you should run ngrok http -subdomain=mysubdomain 5000

5. You'll see a screen giving you the status of your Ngrok connection, and your app should
now be reachable at mysubdomain.ngrok.io .

If you did not sign up for one of the paid Ngrok plans, you'll want to do the following:

1. In your terminal, start Ngrok on your localhost port. If your localhost URL is
localhost:5000 then you should run ngrok http 5000 .

2. You'll see a screen giving you the status of your Ngrok connection, including the random
URL where it can be reached in the "Forwarding" column. It looks something like
http://aa23d212.ngrok.io .

3. Remember, because this isn't a paid plan with custom subdomains, that URL will be
completely random each time you restart the service and you'll need to update your Shopify
app's settings each time it changes.

Again, it's entirely possible to test your Shopify app on localhost. Just be aware that you can't use
embedded apps, proxy pages, webhooks or script tags when you're testing it that way. The
alternatives are to buy your own domain and upload the app to your host before testing those
features, or use a localhost forwarder like Ngrok.

Create a new Shopify app.

11
Chapter 1: Setting up and testing your Shopify app.

After setting up the web project and localhost forwarder, we'll next have to set up (or "provision")
an actual Shopify app using Shopify's partner dashboard. This is the process every app developer
goes through to get their unique Shopify API keys. It's pretty easy to do, but I'll guide you
through it if you've never done it before.

(You can skip ahead to the next section if you've already created an app, just make sure your app
is using the localhost URL as its Redirection URL.)

First, head over to your Shopify partner dashboard. Once logged in you should navigate to the
"Apps" link on the left, and then click the "Create app" button near the top right of the page.

The developers at Shopify like to experiment and change the design/layout of Shopify's
dashboards from time to time, so things may not be exactly where described or may have
slightly different text based on when you're reading this.

You'll be asked whether you want to create a new Custom App (sometimes called a Private
App), or a new Public App. There are three major differences between a Custom App and a
Public App:

1. Custom apps can only be installed on one single store, they cannot be published on the app
store like a public app.
2. Custom apps cannot use Shopify's billing or OAuth APIs, meaning you cannot charge for
their usage (unless you use a third party billing service unrelated to Shopify).
3. Because private apps can't use the OAuth API, they do not have a streamlined installation
process like a public app does; store owners most manually add the app to their store, select
which API permissions to grant it, then send you the new custom app's API keys.

Essentially, a custom app is what you want to use when you're building a dedicated app for one
client or store and do not intend to let anybody else install or use the app. If you want to publish
the app on the app store -- and charge store owners money to use it -- then you want to create a
public app.

In this book we're going to assume you want to use a public app, but the majority of the guide
still applies for custom apps. The only major difference, again, is that you won't be using the
OAuth or billing APIs; and whenever you see the guide talking about access tokens you simply
need to replace the token with your custom app's secret API key.

So to get started you'll need to choose a name for your app, and an app URL. While we're
developing (i.e. before deploying to production on a real server), that app URL is going to be
the forwarded localhost one you set up in the last chapter (e.g. example.ngrok.io if you're
using ngrok).

12
Chapter 1: Setting up and testing your Shopify app.

There are three different URLs you need to enter on this screen. The paths and lower-casing are
important, so pay close attention to what you type:

1. The "App Url", which should look like https://example.localhost-


forwarder.com/shopify/handshake -- this is where users will be sent when they try to

install your app, and when they try to login.


2. A whitelisted redirection URL which is https://example.localhost-
forwarder.com/shopify/authresult -- users will be sent here during the final step of the

app OAuth installation/login process.


3. Another whitelisted redirection URL https://example.localhost-
forwarder.com/shopify/chargeresult -- users will be sent here once they've accepted and

agreed to your billing charges.

Replace the example.localhost-forwarder.com domain in the URLs above with the


domain your localhost-forwarder gave you, for example something.ngrok.io . If you're not
using a forwarder and don't plan on testing or using webhooks, replace the domain with
localhost and the port, e.g. localhost:5000 .

13
Chapter 1: Setting up and testing your Shopify app.

Here's what we're going to do with these URLs: whenever a Shopify user tries to install your app,
or open the installed app from their admin dashboard, they'll be sent to the /shopify/handshake
path which will map to a specific C# controller class and method in the project. The querystring
for this request will include the user's Shopify store URL, which we'll use to determine if they're
trying to login or create an account by checking to see if an account with that store URL exists in
a database.

Assuming they're creating an account, we'll have them go through the registration process and
then send them back to a special URL that Shopify will use to confirm the app installation with
the user. We include the next redirect URL here, which will be /shopify/authresult . If the
user confirms that they want to install the app they'll then be redirected to that path.

This time the querystring will contain all of the parameters we'll need to use to create an access
token, which is essentially a unique password that gives us permission to make calls to the
Shopify API on the shop owner's behalf. With this access token created, we'll then use it to create
a monthly subscription charge, sending the user back to Shopify for a final time to confirm they
want to accept the charge. If they accept it, they'll be redirected to /shopify/chargeresult and
we can activate the subscription.

Anyway, once you've got the URLs set and the app created you'll see a section at the bottom of
the page named "App credentials". These credentials are what you'll use to create shop access
tokens, which are quite literally the keys to using Shopify's API. You'll need to copy both the
API key and the API secret key from this section. We'll add them to the application in the next
chapter.

Note: once you've created the app, you'll find more settings available to you including an
"Extensions" tab. This tab contains options for embedding the app in the store owner's
admin dashboard, which we'll cover later on in this book. For now, you can ignore the extra
settings and just copy your app credentials.

14
Chapter 2: Setting up and using "user secrets"

Setting up and using "user secrets"


The dotnet CLI has a built-in tool for setting "user secrets" on a per-project basis. In essence, a
user secret is something that you as a developer (the "user" in this case) need to keep secret;
something you don't want to check in to source control where naughty denizens of the internet
might steal it.

Does that sound familiar? It should! You already have something in your possession that should
be kept secret and out of the prying eyes of internet ne'er-do-wells: your Shopify secret key.
This should never be checked in to source control, and should never be written anywhere in your
code. The dotnet CLI tool will help you keep the secret key, well, secret, but also help inject it
into your application at run time so it can actually be used too.

Here's how it works: you tell the dotnet CLI which value you want to keep secret, you give it a
name, and then dotnet will store it somewhere else on your computer outside of the application
folder. It adds a unique identifier to your .csproj project file which it can then use to find the
secret when the application runs. Once running, dotnet will use that identifier to load the secret
and inject it into the application's environment variables where you can use it.

So instead of hard coding your Shopify secret key into your C# code, you just look it up by the
secret's name:

Example

var secretKey = configuration.GetValue<string>("SHOPIFY_SECRET_KEY");

This keeps your secret keys safe and secure on your own machine, ensuring they won't
accidentally be checked into source control or published online where they could be stolen. To
get started, open your terminal and in your project directory (the directory with your ASP.NET
project's .csproj file), initialize dotnet user secrets with this command:

Terminal

dotnet user-secrets init

You'll see a message about setting the UserSecretsId to a string of numbers and letters. That's the
unique identifier which the CLI tool will use to find your secrets when the application is running.
Next up, you need to add your secrets to the secret store:

Terminal

15
Chapter 2: Setting up and using "user secrets"

dotnet user-secrets set "SHOPIFY_SECRET_KEY" "secret key goes here"

While your public API key is not something that needs to be kept secret, you should add it to the
secret store anyway just so we can load it easily alongside the secret key:

Terminal

dotnet user-secrets set "SHOPIFY_PUBLIC_KEY" "public key goes here"

The app is going to need your SQL database password too, and you can add it to the user secrets
in the same manner. If you used the example command for quickly setting up a Docker SQL
container from earlier in this guide, the password would be a-BAD_passw0rd . Don't use that
password in production when you deploy your app to the real world!

Terminal

dotnet user-secrets set "SQL_PASSWORD" "a-BAD_passw0rd"

Verify that you've correctly set all of your user secrets by using the list command; you should see
all three secrets and their values:

Terminal

dotnet user-secrets list

And finally, the app will need to know how to connect to your SQL database. You can either add
the SQL connection string to your user secrets, or add it to the ASP.NET AppSettings file which
is the more traditional manner. Inside your project folder, you should have a file named
appsettings.Development.json; if you don't have one, create the file.

Note: the casing of the file is very important. If it doesn't match the name of the
ASPNET_ENVIRONMENT variable -- which is Development by default -- the framework will

not load or read the file and won't find your connection string.

Inside the file, add a JSON section named ConnectionStrings and place your SQL database
connection string inside under the name DefaultConnection . Do not include your SQL
database password in the connection string, as this file will typically be committed to source
control (i.e. git/mercurial/tfs) and you do not want to publish sensitive database passwords to
source code repositories. Instead we'll write code in an upcoming chapter that will combine the
connection string and the SQL password from user-secrets to form a full connection string.

appsettings.Development.json

16
Chapter 2: Setting up and using "user secrets"

{
"ConnectionStrings": {
"DefaultConnection":
"Server=localhost;Database=master;MultipleActiveResultSets=true;User Id=sa"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}

And now whenever you start your application, the ASP.NET framework will see your app
settings file and load the connection string for use inside the application. It's also going to see that
your project has been configured to use user secrets and will load them into sets of environment
variables.

All we need to do now is write a small wrapper class to load the values from those environment
variables so they can be used throughout the project with Dependency Injection. In your project
directory, create a new folder named Models (it may already exist), and inside that folder create
two new C# class files named ISecrets.cs and Secrets.cs.

We're going to create a small interface in ISecrets.cs for working with our secret values, and then
write a class which implements the interface and loads secrets from the environment variables.
This interface and its implementation class will have just two properties: one for the Shopify
secret key and one for the Shopify public key; the SQL password will only be needed in the
Startup.cs file, so we won't add it to this interface.

Models/ISecrets.cs

namespace AuntieDot
{
public interface ISecrets
{
public string ShopifySecretKey { get; }

public string ShopifyPublicKey { get; }


}
}

17
Chapter 2: Setting up and using "user secrets"

And now we can write the class that will implement this interface in Secrets.cs. It needs to load
the values from the environment, and to do that, we'll add a constructor to the class with one
IConfiguration object parameter. The configuration object has direct access to the secret

values, so we can use it to load them inside the constructor.

Note that the name you previously gave to each secret when setting it with the dotnet user-secrets
CLI tool will be the name of its config key -- they're case sensitive!

Models/Secrets.cs

using System;
using Microsoft.Extensions.Configuration;

namespace AuntieDot
{
public class Secrets : IScrets
{
public Secrets(IConfiguration config)
{
string Find(string key)
{
var value = config.GetValue<string>(key);

if (string.IsNullOrWhiteSpace(value))
{
throw new NullReferenceException(key);
}

return value;
}

ShopifySecretKey = Find("SHOPIFY_SECRET_KEY");
ShopifyApiKey = Find("SHOPIFY_API_KEY");
}

public string ShopifySecretKey { get; }

public string ShopifyPublicKey { get; }


}
}

We've created an inline function called Find inside the class constructor, which receives a key
and tries to find a config value or environment variable with a name that matches that key. If it
cannot find the value, it throws an exception. This means our app will "fail fast" right away at
startup if it can't find the Shopify keys, alerting you that the secrets are configured incorrectly.

18
Chapter 2: Setting up and using "user secrets"

A note on deployment: when you deploy your app to a live server, your dotnet user secrets
won't make the journey as they exist only on your personal machine. That's okay though:
your app doesn't actually care where the secret values are coming from, it only cares that
they're accessible through environment variables.

In almost all cases, the service you use to host and deploy your app will let you edit the
environment variables; this is true for major services like Azure and AWS, so you should
have little trouble adding your Shopify secret and public keys when setting up a
deployment.

We haven't gotten to the part where we need to use this new class yet, but you might be
wondering where that IConfiguration object is going to come from. While it's possible to
create a new instance of the object "by hand", we're instead going to rely on ASP.NET's
Dependency Injection service to see that the new class needs a configuration instance in its
constructor and "inject" the instance automatically.

The details of Dependency Injection are beyond the scope of this book (in fact they could be a
book of their own), but to be brief: you can register an interface or class with the Dependency
Injection service, and then you can get access to that object by just adding it to the constructor of
any class managed by ASP.NET -- chiefly, the MVC controller classes. The ASP.NET
framework will take care of instantiating the classes/interfaces/services you need, so you don't
need to worry about e.g. creating a new instance of IConfiguration every time you want to use
it. Thanks to DI, it's just there already.

One neat thing about Dependency Injection is that injected things can access other injected things
(barring circular references). In fact, the new class we just wrote is itself going to be added to
Dependency Injection later on in this guide, which means we can just add it to the constructors of
any MVC controller that needs it and the DI service will take care of passing an
IConfiguration instance to it behind the scenes.

19
Chapter 3: Dynamic application URLs for development and production

Dynamic application URLs for development


and production
Unless you're deploying your app to a public server each time you want to make changes and test
them, you're going to need two different sets of URLs for testing: your localhost URLs (or your
localhost-forwarded URLs like "example.ngrok.io"), and your production URLs (like
"example.com"). We don't want to hard code those URLs in the application, because that means
you'd have to go in and change the code each time you switch between deploying to production
and testing in development.

You certainly don't want to be caught in a situation where you accidentally deploy your
development URLs to a production server, as that's going to break a great deal of your Shopify
OAuth installation/login and subscription processes. Remember, localhost addresses are only
accessible on your own computer! And even if you've got your localhost-forwarder running, the
app itself still needs to be up on your computer and not in a state of active development.

So, to solve the problem of needing two separate sets of URLs without having to hard code them
into the app, we're going to create an interface called IApplicationUrls . It'll have a set of
URLs that the application can use when it needs them, but, like all interfaces, it can easily be
swapped out with different implementations without breaking dependent code.

The implementation we're going to write for this new interface will be called ApplicationUrls ,
and it's going to use an instance of ISecrets to figure out the application's host domain -- the
one you added to dotnet user-secrets. When you deploy your application to production, you just
need to add that HOST_DOMAIN environment variable to the server, and the implementations for
ISecrets (and IApplicationUrls by extension) will pick it up automatically.

Using interfaces here makes it easy to test the URLs when e.g. you're doing unit tests. You can
easily create a TestApplicationUrls class that implements IApplicationUrls , but the test
implementation could hard code the URLs instead of pulling them from the environment. If we're
careful to only the interface version throughout the app, you'll be able to easily drop in different
implementations when or where you want.

There are three URLs that need to be added to the interface:

1. An OAuth redirect URL, which tells Shopify where it needs to send users when it redirects
them back to the app during the OAuth installation process.
2. A subscription redirect URL, which similarly tells Shopify where to send users when
redirecting them back to the app after they accept a monthly app subscription.

20
Chapter 3: Dynamic application URLs for development and production

3. An "app/uninstalled" webhook URL, which tells Shopify where to send event notifications
after a user uninstalls the app.

All three of those URLs will be used in this base AuntieDot project, and the the followup projects
will add more as they're expanded into more advanced features.

To get started, create a new C# file named IApplicationUrls.cs in your Models folder. The
interface is very short, with only the three string URL properties and nothing else:

Models/IApplicationUrls.cs

namespace AuntieDot.Models
{
public interface IApplicationUrls
{
string OauthRedirectUrl { get; }
string SubscriptionRedirectUrl { get; }
string AppUninstalledWebhookUrl { get; }
}
}

And now the implementation class, which is simply called ApplicationUrls . We want this
class to use the HostDomain property from an ISecrets instance, which, thanks to
Dependency Injection, can easily be obtained by adding it to the constructor. DI will take care of
the rest.

In your Models folder, create a new C# class file named ApplicationUrls.cs, then add the
following constructor plus placeholder properties/methods:

Models/ApplicationUrls.cs

using System.Text.RegularExpressions;

namespace AuntieDot.Models
{
public class ApplicationUrls : IApplicationUrls
{
public ApplicationUrls(ISecrets secrets)
{
// TODO: configure the url properties
}

string JoinUrls(string left, string right)


{
// TODO: join left and right url segments
}

public string OauthRedirectUrl { get; }

21
Chapter 3: Dynamic application URLs for development and production

public string SubscriptionRedirectUrl { get; }


public string AppUninstalledWebhookUrl { get; }
}
}

Starting with the JoinUrls method, this is just a simple little helper that's responsible for
joining two segments of a URL (the host domain and a subpath, in this case). Using regular
expressions, it'll make sure there aren't any double slashes in the URL, e.g.
"https://example.com//path/goes/here" won't accidentally end up in production. You won't have
to wonder if your HOST_DOMAIN should or shouldn't end in a slash; this method will ensure it
works either way.

Models/ApplicationUrls.cs

public class ApplicationUrls : IApplicationUrls


{
// ...

string JoinUrls(string left, string right)


{
var trimTrailingSlash = new Regex("/+$");
var trimLeadingSlash = new Regex("^/+");

return trimTrailingSlash.Replace(left, "") + "/" +


trimLeadingSlash.Replace(right, "");
}

// ...
}

The two regular expressions in this method are trimming trailing and leading slashes from any
string they're given. The /+$ expression means "match any slash at the end of the line, no
matter how many there are"; similarly, the ^/+ expression means "match any slash at the
beginning of the line, no matter how many there are".

The class constructor can now use that method to join the host domain with three paths,
completing the three URL properties required by the IApplicationUrls interface. Here are the
URLs we want to use in this implementation:

1. The OauthRedirectUrl should point to "/shopify/authresult" (which you might remember


as one of the URLs we gave to Shopify when configuring the app settings.)
2. The SubscriptionRedirectUrl should point to "/subscription/chargeresult".
3. The AppUninstalledWebhookUrl should point to "/webhooks/app-uninstalled".

Models/ApplicationUrls.cs

22
Chapter 3: Dynamic application URLs for development and production

public class ApplicationUrls : IApplicationUrls


{
public ApplicationUrls(ISecrets secrets)
{
OauthRedirectUrl = JoinUrls(secrets.HostDomain, "/shopify/authresult");
SubscriptionRedirectUrl = JoinUrls(secrets.HostDomain,
"/subscription/chargeresult");
AppUninstalledWebhookUrl = JoinUrls(secrets.HostDomain, "/webhooks/app-
uninstalled");
}

// ...
}

With the ApplicationUrls class fully implemented, we'll be able to add it -- and the ISecrets
interface it implements -- to the Dependency Injection service (in Chapter 4). That means we'll be
able to use these application URLs throughout the application wherever they're needed.

23
Chapter 4: User accounts and Entity Framework

User accounts and Entity Framework


With that little bit of setup code out of the way, it's time to get down to business. Our goal to start
off is building a basic app that will let users install the app, connect their Shopify store, and
accept a monthly subscription charge. Keeping that in mind, we've got four major pieces of code
that we'll need to build into AuntieDot to accomplish its goals:

1. A user account model, which we'll use to store information about the user's Shopify
integration (including their API access token) and monthly subscription data.
2. An authentication mechanism that will track a user's Shopify integration and ensure their
subscription to the app is still valid and active.
3. An MVC controller for handling the various requests used in Shopify's OAuth and
subscription processes.

In this chapter, we're going to start with the user account model and the tools we're going to use
to save that account model to our SQL database.

If you've ever spent a moderate amount of time working with C# and ASP.NET within the last
five years, you've undoubtedly heard the name "Entity Framework" at some point. When
developers worldwide were having a brief, passionate fling with "nosql" document-style
databases like Mongo and CouchDB (this developer included), Entity Framework was in the
background quietly chugging along, becoming better and better at managing tried-and-true SQL
server databases.

Entity Framework is a somewhat "boring" ORM -- object-relational mapper -- which provides a


clean, clear interface for turning your C# classes and models into SQL tables. It handles all of the
SQL queries, executions and mappings behind the scenes, letting you work directly with objects
through the LINQ queries we all love as .NET developers.

For example, if you want to select just one user from your database with an Id of either 5 or
6 , you can write C# code that looks like this:

Example

var user = await usersDatabase.FirstOrDefaultAsync(user => user.Id == 5 || user.Id


== 6);

And in the background, Entity Framework will translate that code into this SQL query:

Example

24
Chapter 4: User accounts and Entity Framework

SELECT * FROM [Users] WHERE [Id] = 5 OR [Id] = 6 LIMIT 1

It's pretty powerful stuff! But what makes Entity Framework even better is that it can handle all
of the SQL table management tedium for you. If you're starting off with a fresh project, like we
are in this book, Entity Framework will read your C# classes and turn them into SQL tables
automatically while handling complicated things like foreign keys, constraints, etc. It's also well
suited for performing SQL migrations, where you add or remove properties from your models
and update the backing SQL tables accordingly. EF will see those changes and generate the SQL
commands for you.

For the base AuntieDot project, there are two different models that we're going to store in a SQL
database: a UserAccount model and an OauthState model. We'll get to the state model later
on in this chapter, but let's briefly talk about the user account model and why we won't be using
another popular framework that is often paired with EF for managing user authorization and
authentication.

The other framework I'm talking about here is called ASP.NET Identity. It's often used for
implementing user login, registration, authentication, roles, role management, password resetting,
logging in to social networks via OAuth, two-factor authentication, and the list goes on. It's
almost always paired with Entity Framework, where it will impose certain restrictions and
requirements on your user models to make all of those features work.

Personally, I feel that this framework adds far too much complication for what most simple web
apps need, which is just a trivial interface for putting in a password and getting an authentication
cookie back. That's why, in this book, we're not going to be using ASP.NET Identity for user
authentication and management. In fact, we're not even going to use user passwords at all!
Rather, we'll rely entirely on Shopify's built-in cryptography scheme for validating OAuth
requests and use that as the login mechanism.

The login flow is going to look something like this:

1. User opens the app.


2. We send them to Shopify's OAuth login URL.
3. Shopify will ask the user to log in to their Shopify store.
4. Once logged in, Shopify sends the user back to the app with extra security parameters in the
querystring.
5. We take those extra parameters and pass them to the ShopifySharp package's
IsValidShopifyRequest function. Using your Shopify secret key, ShopifySharp will do

some cryptography following Shopify's validation scheme, and will return true if the request
passes.

25
Chapter 4: User accounts and Entity Framework

6. If the request passes validation then we know it is authentic, comes from Shopify, and can
be trusted. We log the user in automatically without entering a password or username.

So because we're not doing password hashing, we don't need to introduce a complex framework
like Identity into the application. Rather, the built-in security cookie and session management
provided by ASP.NET itself will be more than enough for what we need.

To be clear: this does not mean that Identity is a bad framework and should never be used. It's an
incredibly useful and powerful tool, but it does come with a lot of extra boilerplate that simply
isn't necessary for the application we're building in this book. You may find that as you build and
grow your Shopify app, Identity may be a good fit for your needs. Luckily it's very easy to add
Identity later on when you reach that point.

Modeling user accounts


Let's brainstorm: if we're using Entity Framework, but we're not using Identity for user
management, what considerations do we need to make when we're modeling the UserAccount
and OauthState classes?

Like most models you're going to store in the database, the user account model will need an Id
property which can be used to look up the full user record whenever the app needs it. The model
will also need a ShopifyShopId property, ShopifyShopDomain and ShopifyAccessToken
property so we can use Shopify's API on behalf of the shop. These are filled in as the user goes
through Shopify's OAuth installation process.

Beyond those properties, the user model should also keep track of a subscription charge Id. It can
be used to determine if the user has subscribed to the Shopify app's monthly recurring charge.
Because we need an access token to use Shopify's billing API, it won't be possible for the user
account to have a subscription charge Id when the account is created; this means the property will
need to be nullable. But we can use that nullability to our advantage: if it's null, we know the user
is not subscribed and they must be sent to the subscription page.

Those are all of the properties we'll need on the user account model for this base application, but
you might want to take a moment to think about any changes you might need to make when you
build out "the real thing". For example, we're going to use Shopify's OAuth service as the login
mechanism, but that means e.g. all of the owners and employees of the store are essentially
sharing the same user account and permissions. If you want your app to support multiple users
per store, you should think about an approach that treats each user model as, well, an individual
user with a unique username and password.

26
Chapter 4: User accounts and Entity Framework

Like mentioned above, though, it's thankfully easy to modify your models using Entity
Framework by creating a new migration with the dotnet CLI (which will be covered later on in
this book).

Let's continue on with the user account model. Create a new C# class file named UserAccount.cs
and put it in the project's Models folder. Start adding the properties from above to the new class:

Models/UserAccount.cs

namespace AuntieDot.Models
{
public class UserAccount
{
public int Id { get; set; }

public long ShopifyShopId { get; set; }

public string ShopifyShopDomain { get; set; }

public string ShopifyAccessToken { get; set; }

public long? ShopifyChargeId { get; set; }


}
}

In Shopify's API, and the ShopifySharp package by extension, almost all identifiers will be
long (a C# shortcut keyword for int64 ). If you try to use an int for the shop Id or

charge Id, you'll find that the number is actually too big to fit in the property and the
runtime will throw an exception.

That's all we need for the user account class, but while we're talking about user models we should
take a quick detour to create another class which will hold a user's "session" data. A session is a
user that is actively logged in to the application, and they're authorized to make requests to
endpoints that require user data. In most web apps, the session data is stored in a secure HTTP
cookie; it's typically hashed by a cryptography framework, which, much like Shopify's request
validation discussed earlier, means the data inside the cookie can be validated and trusted.

If you were using ASP.NET Identity (which we're not in this project), there would be specific
classes and methods that you'd need to use to create a user session and authenticate the user.
However, ASP.NET itself has built-in tools for the hashing and validation of session cookies, so
we should have no problem implementing simple session management ourselves. It all starts with
the Session class we're about to create, and later on we'll write code that will take an instance of
the Session class and put its data inside a secure cookie while logging users in.

27
Chapter 4: User accounts and Entity Framework

It's extremely important to note that session cookies in ASP.NET are not encrypted, they are
only hashed. What that means in plain terms is that anybody who can get their hands on the
cookie can open it up and unhash the data inside -- no password or secret key needed. This not
only includes malicious attackers, but also curious users who know their way around a browser's
built-in dev tools.

Because of how easy it is to peek inside a cookie, you should never store sensitive user
information in it or use the entire UserAccount object as a session. If you did that, the user's
Shopify access token could be stolen, which is very, very bad. The access token is extremely
sensitive, even more so than a password, because any attacker who gets their hands on it will
have instant, direct access to the user's Shopify store via Shopify's API. They could do such
things as deleting all of the orders on a store, changing the product descriptions to something
offensive, or something even more sneaky like siphoning customer data and selling it.

We should never, ever put the access token in a session cookie, or else it's as good as
compromised. Rather, the Session object -- and the cookie by extension -- should only contain
information that is not sensitive.

The solution is to put only the user Id and their Shopify subscription charge Id in the cookie, as
an attacker couldn't do anything with those values except deduce that the user is subscribed to the
app. Then, whenever the app needs to use the access token, it can simply take the user Id from the
session cookie and use it to pull in the full account from the database.

Let's quickly create the Session class, and then end our little detour and get back to Entity
Framework. In the Models folder, create a new C# class file named Session.cs:

Models/Session.cs

public class Session


{
public Session(UserAccount user)
{
UserId = user.Id;
ShopifyChargeId = user.ShopifyChargeId;
}

public Session()
{

public int UserId { get; set; }

public long? ShopifyChargeId { get; set; }


}

28
Chapter 4: User accounts and Entity Framework

In this code, the Session class has two constructors, simply for convenience. One of the
constructors lets you instantly create a session just by passing in an instance of the UserAccount
class; the other lets you assign the properties manually. The code we're going to write in this
project will be using both of these constructors. The former will be used when creating a session
and signing a user in, and the latter will be used when reading a session cookie on subsequent
requests.

So, session detour over, let's get back to Entity Framework! One final model class remains before
we can spin up some SQL tables. This is the OauthState class, which will be used to track and
validate all login requests issued by the application.

We'll get into exactly how that works coming up, but to summarize: each instance of the
OauthState class is going to have a randomly-generated token property, and every time a user

tries to log in to the app that token will be saved to the database. The user gets sent through
Shopify's OAuth process with the token attached, and Shopify will send the token back with the
user as they complete the login or installation process. The app takes that token and checks to see
if it can still be found in the database; if so, the user gets logged in and the token is deleted, but if
not the user must log in again and start the process over.

The goal is to limit the user to one login per token, to prevent the login URL being stolen and
usurped by attackers -- again, something that will be explained in more detail in an upcoming
chapter.

Once more, create a new C# class file in the Models folder named OauthState.cs. It's going to
have three properties: a database Id, a timestamp that tracks when it was created, and the
randomly-generated token string:

Models/OauthState.cs

using System;

namespace AuntieDot.Models
{
public class Oauthstate
{
public int Id { get; set; }

public DateTimeOffset DateCreated { get; set; }

public string Token { get; set; }


}
}

29
Chapter 4: User accounts and Entity Framework

That's it for the model classes! Next up, we need to set up an Entity Framework "database
context" class and then use the EF CLI tool to create SQL tables from the new models. A
database context class is a very simple class that extends EF's DbContext class and specifies
which of our classes act as models (and should thus be turned into SQL tables).

Create a new folder in your project directory named Data and add another new C# class file
named DataContext.cs:

Data/DataContext.cs

using Microsoft.EntityFrameworkCore;
using AuntieDot.Models;

namespace AuntieDot.Data
{
public class DataContext : DbContext
{
public DataContext(DbContextOptions<DataContext> options) : base(options)
{

public DbSet<UserAccount> Users { get; set; }

public DbSet<OauthState> LoginStates { get; set; }


}
}

The constructor for this class is just some boilerplate needed by Entity Framework, telling it how
to connect to the database. Later on in this book we'll set up Dependency Injection and configure
EF to use a SQL connection string, which gets magicked into that
DbContextOptions<DataContext> object behind the scenes.

What's really important here are the two DbSet properties. For every set that appears in an EF
context, EF will create a corresponding SQL database table from the model classes. We can then
write code that uses this new context class throughout the application to get access to the Users or
LoginStates tables.

Creating SQL database tables with Entity Framework


Let's get to the magical part of Entity Framework and invoke some CLI mysticism to set up those
SQL database tables. To start off, the very first thing you need to do with all new projects is
create what EF calls a "base migration". Whenever you hear the word migration in the context of

30
Chapter 4: User accounts and Entity Framework

Entity Framework, what we're really talking about is a series of SQL database transactions which
upgrade or downgrade your database tables/schemas from one version of your models to another,
i.e. after you make changes to a model.

The first migration will migrate the database from a completely empty, blank slate to a version
with two tables: Users and LoginStates (based off of the names you gave the DbSet properties
in the new DataContext class).

Open your terminal, and from your project directory, type the following command to initialize EF
with a new migration:

Terminal

dotnet ef migrations add InitialCreate

Hint: you can name these migrations anything you want. In the command above, the
migration was named "InitialCreate".

If you get an error saying the "dotnet-ef tool" doesn't exist or couldn't be found, you need to
install it with this command:

Terminal

# Install the dotnet-ef tool if the previous command to create a migration failed
dotnet tool install --global dotnet-ef

Assuming the migration command completed successfully, you'll now have a Migrations folder
in your project directory. Inside, you'll probably see three different files, two of which are named
something like {date}_InitialCreate.Designer.cs and {date}_InitialCreate.cs. The final file is a
model snapshot file and is largely irrelevant to us as developers, it just records what your
database context looks like so EF can map it to SQL tables.

Let's open up those new files to see what's inside and get a better look at how these migrations
work. Open the migration in the Migrations folder named {date}_InitialCreate.cs. You'll find a
class that has two methods: Up and Down . Each of those methods calls functions for either
creating SQL tables or dropping (deleting) them. When you apply migrations -- which we'll do
next -- EF is going to call the Up method for every migration that has not yet run. It knows
which migrations have and have not run by managing its own version history table inside your
database.

For this reason, it's extremely important to let Entity Framework manage your database tables.
Don't go editing them by hand or the framework will get out of sync and will begin to throw
exceptions!

31
Chapter 4: User accounts and Entity Framework

If the Up methods are called every time you add a change and update your database, it follows
that the Down methods are called when you need to roll back or revert some of those changes.
Going down is typically only done when you're developing on your personal computer, as it can
easily lead to data loss in production.

To give an example of why you might want to roll back a migration during development, you
could imagine a scenario where you decided to add a new property to the user account model,
and then later decided you don't want to use that property after all. In this case, you'd delete the
property and use the EF CLI tool to roll the database back to an earlier migration.

Again, rolling back your database can easily lead to data loss in production. My own rule of
thumb is that once a migration has been applied to production, it's there to stay forever and will
never be rolled back except in emergencies. And even then, I'd review the Down method for
every single migration being rolled back to ensure I understand exactly what EF is going to do
with the data and tables in my database.

A quick side note about data loss: Entity Framework cannot read your mind or know your
intent. If you start with one property called Name and decide you later want to rename it to
FullName , EF won't know what you're doing is just a simple rename. It's going to think

you want to drop the Name column and all of the names in it. In cases like these, you'd
have to inspect the migration file and modify the code within to perform a column rename
instead of a column drop.

Caveats about data loss aside, we should be ready to update the database and apply the first
migration. The following command will get the ball rolling:

Terminal

dotnet ef database update

If everything goes well, you'll see something like "Build succeeded" in your terminal after the
command finishes. At this point, your database tables have been created. If you have a program
or tool that lets you look inside your database (such as Azure Data Studio), you'll see your brand
new tables!

32
Chapter 4: User accounts and Entity Framework

If instead you get an error when running the EF update command that says something along the
lines of "Unable to create an object of type 'DataContext'", the most likely issue is that you
haven't configured your application to use a SQL Server DbContext in the Startup.cs file. We
covered that a few pages earlier in this chapter!

And finally, if you got an error in your terminal about a "network-related or instance-specific
error occurred while establishing connection to SQL Server", this means your SQL Server is
either not running or your connection string is incorrect. It's also possible that your SQL
password is wrong. We configured the connection string in the Startup.cs file a few pages earlier

33
Chapter 4: User accounts and Entity Framework

in this chapter, so you can go back to double check that you got it correct. If you're using the
Docker container for SQL Server, you can make sure it's running by starting it with this
command:

Terminal

# If you're using Docker container for SQL Server, use this command to start it:
docker start auntiedot-sql-database

34
Chapter 5: Startup and Dependency Injection

Startup and Dependency Injection


You'll often hear the term "dependency injection" bandied about throughout the .NET world --
and in fact it's been bandied about several times already in this very book. Dependency Injection,
or DI for short, is one of ASP.NET's secret weapons; it's surprisingly difficult to implement in
languages like JavaScript without bending over backwards, but it just works for .NET because of
the nature of the framework and the runtime.

While you certainly don't need to use Dependency Injection to build your Shopify app, it will
make your code much cleaner and easier to focus on what you want it to do instead of how you
want to do it. So we'll take this brief chapter to quickly cover what the benefits of Dependency
Injection are and how to set it up with the classes we've already written in this book.

So, what is Dependency Injection exactly and what does it do?

If you imagine a conversation between a developer and the personification of ASP.NET, then DI
is the equivalent of the developer saying to the framework: "please give me an instance of XYZ
interface so I can use it in this class of mine, but you do the instantiating and passing in all the
constructor arguments for me so I don't have to deal with that myself".

According to Microsoft's own guide on Dependency Injection, it can be helpful in three


situations:

1. If you have a RandomUtility class that is used by many other classes, you must modify the
class to change the implementation. DI addresses this by letting you call for interface instead
(e.g. IRandomUtility ). Once your code depends on the interface rather than the
implementation, you can easily switch out the backing class with another class that
implements the interface while making no changes to the code using the interface. This can
be useful in situations where e.g. the class you use changes based on the environment the
app is running in, or any other arbitrary condition.
2. If the RandomUtility class also has its own dependencies, they must be configured and
passed into the constructor each time the class is instantiated. DI addresses this by
configuring and supplying those arguments or dependencies automatically, so you don't
have to instantiate the classes yourself. This makes your code much cleaner and more
focused on what you're trying to do instead of how you're trying to do it.
3. If the RandomUtility class works with external APIs, file systems, databases or any other
kind of production data, it becomes difficult to test the application without interacting and
mutating those things -- potentially muddying or even destroying sensitive data beyond use.
DI addresses this by again abstracting away the implementations of the utility class and

35
Chapter 5: Startup and Dependency Injection

using interfaces that you can mock or fake during testing.

In ASP.NET, dependency injection goes something like this: you create an interface with certain
methods and properties that you envision will be used throughout your app. You then create a
class that implements that interface, and you add both the class and the interface to ASP.NET's
built-in DI service in Startup.cs. Then, in any MVC controller class (or any other class managed
by ASP.NET), you just add the interface as one of the arguments to the controller's constructor.
Your controller now depends on that interface, and the framework will magically see that, find
the class implementation, and pass it to the controller.

In the AuntieDot Shopify app, we have two classes called Secrets and DataContext that we
want to add to Dependency Injection. They'll be used throughout the MVC controller classes
we're going to write in the next chapter. Configuration of DI services all takes place during
application startup, the methods for which can be conveniently found in the Startup.cs file.

Much of the startup file contains boilerplate from the dotnet project template, and we won't have
to mess with it too much. Let's start off by adding all of the using statements we'll need at the top
of the file. Note that your startup file likely already contains some of these using statements and
functions, just add what's missing (particularly the arguments for each function).

Startup.cs

using System;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.EntityFrameworkCore;
using AuntieDot.Data;
using AuntieDot.Models;

namespace AuntieDot
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}

public IConfiguration Configuration { get; }

public void ConfigureServices(IServiceCollection services)


{

36
Chapter 5: Startup and Dependency Injection

// ...
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)


{
// ...
}
}
}

There are just a couple of things that need to be done to this class to get it working the way we
need it:

1. Configure the application to use cookie authentication and authorization.


2. Add a method for building the SQL database connection string by combining the connection
string from appsettings.Development.json and the database password that you added to
dotnet user-secrets.
3. Add the DataContext , Secrets and ApplicationUrls classes to Dependency Injection
services.

Let's start off with configuring the cookie authentication and authorization. Add a new method
called ConfigureCookieAuthentication to the class, and add a CookieAuthenticationOptions
object as one of the method's arguments. The method is going to use that object to configure how
long authentication cookies should last, and which paths the user should be redirected to if they
aren't signed in or their authentication cookie has expired:

Startup.cs

public class Startup


{
// ...

private void ConfigureCookieAuthentication(CookieAuthenticationOptions


options)
{
options.Cookie.HttpOnly = true;
options.SlidingExpiration = true;
options.ExpireTimeSpan = TimeSpan.FromDays(1);
options.LogoutPath = "/Auth/Logout";
options.LoginPath = "/Auth/Login";
options.AccessDeniedPath = "/Auth/Login";

options.Validate();
}

// ...
}

37
Chapter 5: Startup and Dependency Injection

In the code block above, we're configuring the authentication cookie to expire after just one day,
but also to continue pushing back that expiration date each time the user uses the app
( options.SlidingExpiration = true ). This method is also configuring the application to send
the user to /Auth/Logout when the user wants to log out, and /Auth/Login when they want to
log in or they try to access a secure part of the app. Those paths are going to connect to an
authentication controller class later on in this book.

Next up, configuring the SQL connection string. Remember a couple chapters back when you
added a partial SQL connection string to the appsettings.Development.json file? We're going to
pull that connection string out of that file, and then combine it with the SQL database password
from dotnet user-secrets. This is pretty simple thanks to .NET's built-in
SqlConnectionStringBuilder class, and we can easily pull in both the connection string and the

password using the IConfiguration instance that gets set on the startup class in its constructor.

Startup.cs

public class Startup


{
// ...

private string GetSqlConnectionString()


{
var partialConnectionString =
Configuration.GetConnectionString("DefaultConnection");
var password = Configuration.GetValue<string>("sqlPassword");
var connStr = new SqlConnectionStringBuilder(partialConnectionString)
{
Password = password,
Authentication = SqlAuthenticationMethod.SqlPassword
};

return connStr.ToString();
}

// ...
}

And with those two functions, we can configure our app's services and features in the
ConfigureServices method. This is also where we configure Dependency Injection. We're

going to do five separate things in the service configuration:

1. Configure the app to use cookie authentication using the method we wrote above.
2. Configure the app to use MVC controllers with views (this is a boilerplate method that's
probably already included in your project template).

38
Chapter 5: Startup and Dependency Injection

3. Configure the app to use Entity Framework and the DataContext class, passing in the SQL
connection string from the method written above. Adding Entity Framework will
automatically add the data context to Dependency Injection too.
4. Configure the app to add ISecrets and its implementation Secrets to Dependency
Injection.
5. Configure the app to add IApplicationUrls and its implementation ApplicationUrls to
Dependency Injection. This one must come after ISecrets , because its constructor relies
on it to get the HostDomain property.

Startup.cs

public class Startup


{
// ...

public void ConfigureServices(IServiceCollection services)


{
services.AddControllersWithViews();
// Add cookie authentication
var authScheme = CookieAuthenticationDefaults.AuthenticationScheme;
services
.AddAuthentication(authScheme)
.AddCookie(ConfigureCookieAuthentication);
// Add Entity Framework and the DataContext class
services
.AddDbContext<DataContext>(options =>
options.UseSqlServer(GetSqlConnectionString()));
// Add ISecrets and Secrets to Dependency Injection
services.AddSingleton<ISecrets, Secrets>();
// Add IApplicationUrls and ApplicationUrls to Dependency Injection
services.AddSingleton<IApplicationUrls, ApplicationUrls>();
}

// ...
}

One minor thing to note about the code block above: we're using services.AddSingleton to add
the secrets interface/class to DI. There's another method called services.AddScoped that does
almost the same thing. The difference is that when using AddSingleton , ASP.NET will only call
the constructor for the class one single time, and it will reuse it for all further requests. Whereas
with the AddScoped method, ASP.NET will construct a new instance each time the class is used.

With the services configured, we now need to tell ASP.NET to turn on and use those services.
We do that in the Configure method, which contains a lot more boilerplate from the dotnet
template than the previous method did. Without any changes, yours probably looks something

39
Chapter 5: Startup and Dependency Injection

like this:

Example

public class Startup


{
// ...

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)


{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
}

Again, your Configure method probably looks something like that. If it's a little bit different,
that's okay. The important part is the couple of lines we're about to add to this method:

1. A line of code that turns on status code pages, which display messages like 404 not found
when files aren't found instead of blank, empty pages.
2. Turn on authentication. This will tell the app that we want to track who is logged in and who
is logged out. You'll notice that the app is already using authorization, but we still need to
add authentication which is slightly different. Both are required to make logging users in and
out with cookies work properly.

Startup.cs

public class Startup


{
// ...

40
Chapter 5: Startup and Dependency Injection

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)


{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
+ app.UseStatusCodePages();
app.UseHttpsRedirection();
app.UseStaticFiles();
+ app.UseAuthentication();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
}

Voila! That's all it takes. We've now configured the app to use cookie authentication, Entity
Framework and Dependency Injection.

41
Chapter 6: Authentication extensions and validation attributes

Authentication extensions and validation


attributes
We're almost ready to dive into the Shopify OAuth process and the controller code that will
handle the cool parts of the Shopify application (e.g. subscribing a user to a monthly plan,
retrieving a list of their Shopify orders, etc.). But first, we need to write a little more plumbing
code to deal with validation of requests coming from Shopify, and the authorization of users.

Let's start with the most basic building block, which is going to be an extension to sign a user in.
While ASP.NET does have a built-in SignInAsync extension on the HTTP context object, it
requires you to pass in a "user principal" -- essentially a dictionary of properties that describe the
user. We need to do that no matter what, but we can prevent repeating ourselves (and potentially
introducing bugs) by centralizing it all in our own custom extension method.

Create a new folder in your project directory named Extensions and add a new C# class named
HttpContextExtensions.cs inside. Since we're writing extension methods, the class and all of its
methods need to be static .

Start off with two methods named SignInAsync which are async (obviously), return an empty
task, and whose first parameter is this HttpContext ctx . One of these two methods will take a
second Session parameter, and the other will take a second UserAccount parameter.

Extensions/HttpContextExtensions.cs

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Http;
using AuntieDot.Models;

namespace AuntieDot.Extensions
{
public static class HttpContextExtensions
{
public static async Task SignInAsync(this HttpContext ctx, Session
session)
{
// TODO: sign the user in

42
Chapter 6: Authentication extensions and validation attributes

public static async Task SignInAsync(this HttpContext ctx, UserAccount


userAccount)
{
// TODO: sign the user in
}
}
}

It's very important when creating extension methods that the class and method are both
static , and all of the extension methods reference a this parameter. Without those two

things, you'd need to instantiate the extension class each time you want to use its methods.
By making it static and using this to refer to the thing you want to extend, the methods
will appear directly on the extended class as though they were written that way originally.

As mentioned above, to sign a user in with ASP.NET, you need to create an "identity". At its very
core, an "identity" is just a list of properties that describe the user. We can add whatever we want
to this list, and the values will be attached to the authentication cookie. Remember, this means
the cookie can be viewed and stolen by anybody savvy enough to inspect cookies with their
browser's dev tools.

In our case, we only need to add the user's ID to the identity/cookie, along with a flag indicating
whether they're subscribed to the application's monthly plan. For everything else, if our
application needs more sensitive user data to handle a request, it can take the user ID from the
cookie and pull in the full user account from the database.

So with that in mind, we can flesh out the first SignInAsync method -- the one that takes a
session parameter -- to turn the session into an identity. Once the identity is created, you pass it to
an identity "principal", and pass that principal to the HTTP context as the last step to sign the user
in with ASP.NET. The other SignInAsync method will simply convert a full user account
instance to a session using the session constructor we wrote a couple of chapters back, and then
pass it to the first SignInAsync method.

Extensions/HttpContextExtensions.cs

public static async Task SignInAsync(this HttpContext ctx, Session session)


{
var claims = new List<Claim>
{
new Claim("UserId", session.UserId.ToString(), ClaimValueTypes.Integer32),
new Claim("IsSubscribed", session.IsSubscribed.ToString(),
ClaimValueTypes.Boolean)
};
var authScheme = CookieAuthenticationDefaults.AuthenticationScheme;

43
Chapter 6: Authentication extensions and validation attributes

var identity = new ClaimsIdentity(claims, scheme);


var principal = new ClaimsPrincipal(identity);

await ctx.SignInAsync(principal);
}

public static async Task SignInAsync(this HttpContext ctx, UserAccount


userAccount)
{
await SignInAsync(ctx, new Session(userAccount));
}

That's all it takes to sign a user in to the application: adding the properties we want to the list of
claims and turning it into an Identity, then turning that Identity into a Principal. After calling this
new extension method, all future requests to the application will show the user as authenticated,
which you can (and we will) check using the boolean property
HttpContext.User.Identity.IsAuthenticated .

But what goes in, must come out! And it doesn't come out automatically, so we need to write
another extension method that's going to convert an authentication cookie back into a Session
instance. Luckily doing so is just as easy by writing an extension method for this
ClaimsPrincipal userPrincipal -- that's the User part of HttpContext.User -- which will

look through the principal's list of claims and pluck them back out into a Session.

In the same extension class, add a new static method named GetUserSession . The first thing
this new method should do is check that the user is in fact signed in before trying to convert the
cookie to a session. After that, it's just a matter of using a small property lookup function to find
the properties we're after and converting them to properties on the Session:

Extensions/HttpContextExtensions.cs

public static class HttpContextExtensions


{
// ...

public static Session GetUserSession(this ClaimsPrincipal userPrincipal)


{
if (!userPrincipal.Identity.IsAuthenticated)
{
throw new Exception("User is not authenticated, cannot get user
session.");
}

// An inline function that looks for properties on the user principal and
converts them
// to the desired value type (e.g. int, bool, string, etc.)
T Find<T>(string propertyName, Func<string, T> valueConverter)

44
Chapter 6: Authentication extensions and validation attributes

{
var claim = userPrincipal.Claims.FirstOrDefault(claim => claim.Type ==
claimName);

if (claim == null
{
throw new NullReferenceException($"Session claim {claimName} was
not found.");
}

return valueConverter(claim.Value);
}

var session = new Session


{
UserId = Find("UserId", int.Parse),
IsSubscribed = Find("IsSubscribed", bool.Parse)
};

return session;
}
}

The way we're using the inline Find<T> function here might look a little bit confusing or
foreign if you've never used functional programming languages or design patterns, but hopefully
it's not too mystifying. Since the UserId property needs to be parsed from a string value to an
integer value, we pass in int.Parse as the "value converter" function. The same goes for the
IsSubscribed property, which needs to be parsed from a string to a boolean with bool.Parse .

The function will call those value converter functions, converting the strings to ints and bools.

This sort of passing methods around as arguments is extremely common in C#'s more functional
sibling language F#, but you don't see it quite as often in C# projects. The code could be rewritten
to look more like this:

var session = new Session


{
UserId = Find("UserId", str => int.Parse(str)),
IsSubscribed = Find("IsSubscribed", str => bool.Parse(str))
}

And it would still work just the same. It's a matter of personal preference, but since I'm primarily
an F# developer and love evangelizing the language, I'm prone to taking little functional shortcuts
where I can.

45
Chapter 6: Authentication extensions and validation attributes

Those three methods make up all of the extension methods we'll need to write in this sample
application, so let's move on to writing two custom authorization/validation attributes. These
attributes will let us decorate a controller action, or indeed an entire controller, forcing all
requests to the action or controller to first run through the code we write in the attribute.

In practice, you can use attributes for a variety of scenarios such as requiring certain roles from
users; checking request headers; or logging certain details. These attributes are similar to the
middleware that is also supported by the framework, where all requests run through arbitrary
functions/methods and those functions/methods can stop execution of the request before they get
to a controller class.

For this sample project, we're going to write two attributes:

1. One to require that a user making requests to a controller or action have subscribed to the
application before accessing the endpoint.
2. One to validate requests coming from Shopify are authentic, i.e. they haven't been faked or
spoofed by malicious attackers.

We'll start simple with the first attribute. It's going to check if the user making a request is
authenticated, get their session from the authentication cookie using the GetUserSession
extension method we just wrote, and then check if the user has subscribed to the app by looking
at that session. If they have, they can access whatever controller or action they were attempting to
access; if they haven't, though, they'll be redirected to a /Shopify/Subscribe route (which we're
going to build out in a couple of chapters).

They call this kind of attribute an "authorization filter", and ASP.NET has an
IAuthorizationFilter interface we need to implement to indicate the class we're writing deals

with authorization. Since we want to use this class as an attribute, it also needs to extend
ASP.NET's built-in AuthorizeAttribute , which is a simpler version of what we're building that
only checks if the user is logged in.

In your project directory, create a new folder named Attributes and inside create a new C# class
file named AuthorizeWithActiveSubscriptionAttribute.cs.

The attribute class is extremely simple, it just needs to implement the interface's
OnAuthorization method where we get the user session and check if it's subscribed:

Attributes/AuthorizeWithActiveSubscriptionAttribute.cs

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using AuntieDot.Extensions;

46
Chapter 6: Authentication extensions and validation attributes

namespace AuntieDot.Attributes
{
public class AuthorizeWithActiveSubscriptionAttribute : AuthorizeAttribute,
IAuthorizationFilter
{
public void OnAuthorization(AuthorizationFilterContext ctx)
{
// Check if the user is authenticated first
if (!ctx.HttpContext.User.Identity.IsAuthenticated)
{
// The base class will handle basic authentication
return;
}

// Get the user's session and check if they're subscribed


var session = ctx.HttpContext.User.GetUserSession();

if (!session.IsSubscribed)
{
// Redirect the user to /Subscription/Start where they can start a
subscription
ctx.Result = new RedirectToActionResult("Start", "Subscription",
null);
}
}
}
}

Simple! If the user is not authenticated, the base auth attribute will handle redirecting them to the
login page; likewise, if the user is not subscribed, the new attribute will send them to a page
where they can start their subscription. And if the user is both authenticated and subscribed, the
attribute will do nothing which by default lets them continue on their merry way to whatever
page or resource they were attempting to access.

To use this attribute, all you need to do is decorate a controller or a controller action with it:

Example

[AuthorizeWithActiveSubscription]
public class ExampleController : Controller
{
public IActionResult Index()
{
// Any user who reaches this action will be logged in and subscribed
}

[AuthorizeWithActiveSubscription]
public IActionResult ExampleAction()
{

47
Chapter 6: Authentication extensions and validation attributes

// This is redundant because the controller itself is using the attribute,


but the
// attribute can be applied to actions as well where necessary.
}
}

In C# and F#, you can drop the "Attribute" suffix from your class name when decorating
classes or methods. That's why, in the example above, we can use
[AuthorizeWithActiveSubscription] instead of

[AuthorizeWithActiveSubscriptionAttribute] .

If this new attribute is applied to a controller, it will filter all requests to the controller through the
class we just wrote. If it's applied to an action, it will only filter the requests to that action. That
would let you do things like make a controller with actions (URLs) that are generally public and
don't require authentication, but still lock down one or two of the actions on the controller to only
those who are logged in and subscribed.

One more attribute class remains, and then we can move on to writing some real controller code.
This one is named ValidateShopifyRequest , and it's a bit more complicated than the last
attribute.

Whenever Shopify sends a request to your app, they will include values somewhere in that
request which can be used to confirm that the request originated from Shopify. These requests
from Shopify can take one of several forms, and the validation method is different for each:

1. If the request originated from a user navigating somewhere on Shopify to your app (e.g.
when they install your app or want to log in and use it), Shopify will attach a "signature"
querystring value when they send the user back over. The querystring contains several other
values that, when combined with your app's secret key, should create a hash equal to the
signature value. If the signature you compute is equal to the signature in the querystring, the
request can be trusted.
2. If the request originated from a Shopify webhook, Shopify will attach a header to the request
which contains another signature. This time, you sign the entire request body with your app's
secret key, which will create a hash equal to that signature value. Just like the querystring
signature, if your hash comes out equal to the signature they sent in the headers, you can
trust the request.
3. If the request originated from a Shopify proxy page (a page on your app that runs "natively"
on your users' storefronts -- which we will cover in this book), the request will again contain
a "signature" querystring value that can be computed using your secret key. Once again, if
they're equal then the request can be trusted.

48
Chapter 6: Authentication extensions and validation attributes

For the base AuntieDot project, we'll only need to deal with validating the first type of request
from users installing/logging in to the app. Once we start expanding the base project in upcoming
chapters, we'll cover working with proxy pages and webhooks as well.

Alright, let's write the second validation attribute class. In your Attributes folder, create a new
class file named ValidateShopifyRequestAttribute.cs. This time the class is going to extend the
ActionFilterAttribute . We don't use the same authorization attribute as the last class we

wrote, because this code doesn't care if the user is authenticated or not. It only cares about the
values in the request itself.

The class will need to override a method from its base attribute named
OnActionExecutionAsync , which receives an action executing context, and an action executing

"delegate". The delegate is just a fancy word for a function that we call, which tells the
framework that we're done validating and it should let the user continue with the request. It
follows that if the delegate does not get called, the framework will not continue the request. In
most cases that means it will either drop it entirely and return a blank page, or it will return
whatever result you want to give it.

Attributes/ValidateShopifyRequestAttribute.cs

using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using AuntieDot.Models;
using ShopifySharp;

namespace AuntieDot.Attributes
{
public class ValidateShopifyRequestAttribute : ActionFilterAttribute
{
public override async OnActionExecutionAsync(ActionExecutionContext ctx,
ActionExecutionDelegate next)
{
// TODO: check if the request passes Shopify's validation scheme
}
}
}

Luckily, we don't have to do any fancy cryptography or complicated signature hashing here,
because the ShopifySharp package handles all of that. Instead, the attribute just needs to gather
the request querystring alongside your app's secret Shopify key, and pass it all to ShopifySharp.
The package will return either true or false indicating whether the request can be trusted or not.

49
Chapter 6: Authentication extensions and validation attributes

Getting the request querystring here is easy -- just use the action execution context to access it.
But to get our hands on the Shopify secret key, we'll need to use dependency injection to get an
instance of ISecrets . However, due to the nature of attributes, this is the one place that
ISecrets can't just be added to the class constructor. The reason for it is largely a technical

limitation of attributes, but it boils down to the fact that when you use an attribute, you're calling
its constructor. That means you'd have to pass in whatever DI services it requires before the
things you're decorating even have access to them. That can be extremely hard, if not impossible.

Thankfully, class constructors are not the only way DI services can be accessed. They can also be
accessed by using HttpContext.RequestServices , which holds references to all DI services that
have been added to an app. We just so happen to have an instance of that context available in this
attribute, thanks to the ActionExecutingContext variable.

So, to glue it all together: inside of the new attribute's single method, use the request services to
pull in an instance of ISecrets , then use ShopifySharp to check if the request is authentic by
passing in the secret Shopify key alongside the request querystring. If the request is authentic, the
attribute can call the next action delegate function; and if it's not authentic, the attribute will
return a ForbidResult which just tells the framework to send the user to the login page.

Attributes/ValidateShopifyRequestAttribute.cs

public override async OnActionExecutionAsync(ActionExecutionContext ctx,


ActionExecutionDelegate next)
{
var secrets = (ISecrets)
context.HttpContext.RequestServices.GetService(typeof(ISecrets));
var querystring = context.HttpContext.Request.Query;
var isAuthentic = AuthorizationService.IsAuthenticRequest(querystring,
secrets.ShopifySecretKey);

if (isAuthentic)
{
// Call the delegate to let the request go through to the next action
await next();
}
else
{
// Forbid the request, showing a login screen
context.Result = new ForbidResult();
}
}

A simple bit of code, but that will protect your application and your users from attackers. When
we protect the OAuth login/installation controller with this attribute, it will not be possible for
attackers to spoof a login request as long as they do not have your Shopify secret key. If your

50
Chapter 6: Authentication extensions and validation attributes

secret key does wind up in the wrong hands, attackers will be able to produce a hashed signature
value that matches Shopify's validation scheme, and they'll be able to log in as any user.

Keep that secret key safe!

51
Chapter 7: Handling Shopify's OAuth installation/login process

Handling Shopify's OAuth installation/login


process
It's time to write the interesting parts of a Shopify application! We can now start to flesh out the
user creation/OAuth integration stuff, which will all be under the jurisdiction of a
ShopifyController class. In ASP.NET, route and URL paths are all assigned to controller

classes based on a controller's name, meaning a ShopifyController class would handle all
requests to the "/shopify" URL path; similarly, a FooController class would handle all requests
to "/foo", and so on.

The public methods on this class -- called Actions in ASP.NET -- handle requests to URL
subpaths. That is to say, a ShopifyController.Handshake action would handle requests to
"/shopify/handshake", and a ShopifyController.AuthResult action would handle requests to
"/shopify/authresult".

One thing to note though is that this is just the default routing implementation for ASP.NET.
Paths can be overridden and customized with attributes that we won't go into in this book.

The MVC in ASP.NET MVC stands for Model-View-Controller, which is a very common
design pattern found across a wide variety of programming languages and web frameworks. To
put it simply, it's a separation of concerns. The Controller receives web requests, and is
responsible for creating the Model. The Model holds data that is passed to the View. The View
then uses the Model to render dynamic HTML web pages, which the Controller returns to the
browser.

Many of the languages and frameworks that implement the MVC pattern use a special type of file
for their Views. It's hard to make plain old HTML work when you need to dynamically change
what the web page looks like based on the Model. In ASP.NET, our views are .cshtml files --
commonly called Razor files or Razor pages -- which mix your regular old HTML with our
favorite C# programming language.

For example, pretend you want to write a web page that dynamically shows a different name
based off of a parameter from the URL. Visiting "myapp.com/hello/joshua" would show "Hello
Joshua", and visiting "myapp.com/hello/laura" would show "Hello Laura". This would be
difficult to do in a plain old HTML file without using JavaScript, but in Razor (.cshtml) views, it
can be done quite easily. All it takes is the Controller looking at the requested URL, creating a
Model which contains the name from the URL, and passing it to the View where it's turned into
HTML.

52
Chapter 7: Handling Shopify's OAuth installation/login process

The controller would look something like this:

Example

public class HelloController : Controller


{
[Route("{name}")]
public ViewResult Index([FromRoute] string name)
{
var model = new HelloModel
{
Name = name
};

return View(model);
}

public class HelloModel


{
public string Name { get; set; }
}
}

And then you'd have a separate Razor view file in a folder at Views/Hello/Index.cshtml, which
would use the model to render a greeting message:

Example

@model HelloController.HelloModel
@{
// Make sure the name is capitalized
string name;

if (string.IsNullOrEmpty(model.Name))
{
name = String.Empty;
}
else
{
var firstLetter = model.Name[0].ToUpper();
var rest = model.Name.Substring(1).ToLower();
name = firstLetter + rest;
}
}

<div>
<h1>Hello @name!</h1>
</div>

53
Chapter 7: Handling Shopify's OAuth installation/login process

In this chapter, we're going to write two controllers to handle the full Shopify OAuth
installation/login flow, along with several models and views to support the controllers.

Let's start with the model classes we'll need for the two new controllers. These are the classes that
get instantiated by the controllers and passed to the views. There are two models to start with, and
more will be written later on in this guide. The first is a model for a generic error page, and the
second is a model for the login form.

Your project might already have a folder called Models, but if it doesn't you should create one.
Inside that folder, create a new C# class file named ErrorViewModel.cs. This model is going to
be used by the application's global error handler, so that any time the application runs into an
unexpected error, the user will at least see a nice error message instead of a big ugly .NET
exception stack trace. The model has two properties: RequestId and a boolean called
ShowRequestId which is only true when the request ID is not null or empty:

Models/ErrorViewModel.cs

using System;

namespace AuntieDot.Models
{
public class ErrorViewModel
{
public string RequestId { get; set; }

public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);


}
}

The request ID is a random unique string that ASP.NET creates for each web request. With
proper application logging -- a different topic that sadly goes beyond the scope of this book -- a
user could send you the request ID and you could use your logs to track down what happened
during that specific request.

Moving on, the second view model is for the login page. Create a new C# file in your Models
folder named LoginViewModel.cs. In most applications, this view model would probably have a
username and a password string, but in this application we're using the Shopify OAuth system for
authentication. That means the view model only needs one ShopDomain string property, and an
Error property that will be used to show error messages in the login form when necessary.

For convenience, a ShowError boolean will be used that will be true when the error message is
not null or empty:

Models/LoginViewModel.cs

54
Chapter 7: Handling Shopify's OAuth installation/login process

using System;

namespace AuntieDot.Models
{
public class LoginViewModel
{
public string ShopDomain { get; set; }

public string Error { get; set; }

public bool ShowError => !string.IsNullOrEmpty(Error);


}
}

55
Chapter 8: Signing users up for a monthly subscription plan

Signing users up for a monthly subscription


plan
Let's take a moment to recap where at and what remains. We've created a database for our
application which stores both user data (via the UserAccount class), and login attempts (via the
OauthState class). Whenever a user clicks on the app's Install button in the Shopify app store,

they'll be sent to the ShopifyController.Handshake action. They'll also be sent to that action if
they've already installed the app and are just trying to log in and use it.

Whether the user is installing or logging in, the ShopifyController.Handshake action will
check if the request passes Shopify's validation scheme by using the ValidateShopifyRequest
attribute. Assuming the request is valid, the action sends the user to the next step which is the
login form rendered by AuthController.Login . That form asks the user to enter their Shopify
shop domain -- e.g. "http://example.myshopify.com" -- and tries to autofill it when possible.

Once submitted, the login form gets sent to the AuthController.HandleLogin action. That
action first checks if the shop domain is indeed a valid Shopify shop, and if so, it will save a new
login request to the database as an OauthState object. It then creates an OAuth URL with
ShopifySharp and sends the user there to accept the request API permissions.

As long as the user approves the permissions you've requested, they'll be sent back to the app to
finish the OAuth process. This time, the ShopifyController.AuthResult action handles the
request, once again verifying that its authentic. At this point, the code value that Shopify
attached to the querystring will be exchanged for a permanent API access token, and the user's
account will either be created (if it didn't already exist) or updated with the new token value.

After all of that, the OAuth process is complete and the user is logged in. They're sent to the
home page if they're subscribed, or to a page where they'll be asked to start a monthly
subscription.

That's everything we have so far. Now it's time to implement the SubscriptionController
which will do two different things:

1. Show a subscription details page, which users can visit any time after they've subscribed.
This page will tell them how much their subscription costs, the name of the subscription
plan, when the free trial will end (if applicable), and when they'll be charged next.
2. Start and finish the new subscription process. This process is very similar to the OAuth
process where a unique URL needs to be generated, the user is sent there, and they're asked
to accept the subscription.

56
Chapter 8: Signing users up for a monthly subscription plan

Unlike Shopify's OAuth process, their subscription process doesn't have the same security
safeguards built around it. There are no cryptographic signatures to compute, or state tokens to
store in the database. The security is sort of "built in" to the process itself -- you need a Shopify
access token to create a subscription in the first place, so the user needs to be logged in at this
point already. And when they return after accepting the charge, you'll find that Shopify adds a
charge_id to the querystring which can be used to look up and finalize their charge.

Here's exactly how the subscription process will work in this base AuntieDot project:

1. We show the user a form that asks them to start a monthly subscription charge.
2. When the form is submitted, the app pulls the user's Shopify access token out of the
database.
3. ShopifySharp's RecurringChargeService is used to create a new monthly recurring charge.
The service will return a charge object with a ConfirmationUrl property where the user
needs to be sent to accept the charge. The charge is not yet active until the user accepts it.
4. The user is shown a prompt by Shopify asking them if they want to accept the recurring
charge.
5. After accepting the charge, the user is sent back to the app which looks for a charge_id
value in the querystring. The app uses that ID to pull in the charge object with ShopifySharp
and Shopify's API.
6. The charge object will have one of several statuses including "pending", "declined",
"accepted", "expired" and "active". The charge can only be activated if the status is
"accepted".
7. If the status is "accepted", we activate the charge using ShopifySharp. This will start the
subscription, and the user will be charged immediately unless you configured the charge to
use a free trial period; in that case, the user will be charged at the end of the free trial.

Like the previous controllers, this one will need its own models and views: one view to display
the subscription details after the user has subscribed, and one view to show a form asking them to
start their subscription. Each view will need its own model, so that's a good place to get started.

In your Models folder, create two new C# class files named SubscriptionViewModel.cs and
SubscribeViewModel.cs.

The first model (SubscriptionViewModel.cs) will contain all of the details of a user's Shopify
subscription charge, including the name of the plan they're on, the price, whether it was created in
test mode, and when the free trial ends. Since we'll always want to instantiate this model with all
of those details, we can simply add a ShopifySharp.RecurringCharge instance as one of the
constructor parameters:

Models/SubscriptionViewModel.cs

57
Chapter 8: Signing users up for a monthly subscription plan

using System;
using ShopifySharp;

namespace AuntieDot.Models
{
public class SubscriptionViewModel
{
public SubscriptionViewModel(RecurringCharge charge)
{
ChargeName = charge.Name;
Price = charge.Price.Value;
TestMode = charge.Test == true;
DateCreated = charge.CreatedAt.Value;
TrialEndsOn = charge.TrialEndsOn;
}

public string ChargeName { get; }

public decimal Price { get; }

public bool TestMode { get; }

public DateTimeOffset DateCreated { get; }

public DateTimeOffset? TrialEndsOn { get; }

public bool IsTrialing => TrialEndsOn.HasValue;


}
}

Now whenever the model is instantiated, it will set up its own properties using that charge.

Next up, the `SubscribeViewModel.cs file. This model is extremely simple, it's only going to be
used to show an error message to the user in cases where something goes wrong while the app
tries to start the subscriptin process:

Models/SubscribeViewModel.cs

namespace AuntieDot.Models
{
public class SubscribeViewModel
{
public string Error { get; set; }

public bool ShowError => !string.IsNullOrWhiteSpace(Error);


}
}

58
Chapter 8: Signing users up for a monthly subscription plan

And with those two models written, we can move on to the two views that use them. In your
Views folder, create a new folder named Subscription and inside that folder create two new Razor
view files named Index.cshtml and Start.cshtml.

Starting with Views/Subscription/Index.cshtml, this view is the one that will render the user's
subscription details once they're subscribed. Using the SubscriptionViewModel , this view
should show the user the following things:

1. Whether they're in a free trial, and when that trial is ending.


2. Whether the charge was created in test mode, which means it will not charge real money.
This is purely for our benefit as developers when testing the application.
3. The name of the plan the user is on.
4. The price of the plan, which should be formatted so that "$14" becomes "$14.00".
5. The date their subscription started.

Here's what that should look like:

Views/Subscription/Index.cshtml

@model SubscriptionViewModel
@{
ViewData["Title"] = "Your Subscription";
// Format the decimal price to two places, e.g. "14" becomes "14.00"
var formattedPrice = "$" + Model.Price.ToString("f2");
}

<div class="text-center">
<h2>Your Subscription Details</h2>
<h5>
@if (Model.IsTrialing)
{
<span class="badge badge-pill badge-primary">Free Trial</span>
}
@if (Model.TestMode)
{
<span class="badge badge-pill badge-secondary">Test Mode</span>
}
</h5>
<hr />
</div>
<div>
<ul>
<li>
Plan: @Model.ChargeName
</li>
<li>
Price: @formattedPrice / month
</li>

59
Chapter 8: Signing users up for a monthly subscription plan

<li>
Subscribed on: @Model.DateCreated.ToString()
</li>
@if (Model.IsTrialing)
{
<li>
Trial ends on: @Model.TrialEndsOn.ToString()
</li>
}
</ul>
</div>

Easy! Once the application is up and running, that page will look something like this:

Following that, the last file before we can start writing the new controller is the
Views/Subscription/Start.cshtml. This one is slightly shorter than the previous one, it just needs to
show a button to the user asking them to start their subscription, and an error if something goes
wrong.

While this book doesn't cover using multiple different subscription plans, this is the place you'd
want to do that. You can have as many different plans as you want in your application, so your
imagination is the limit. For example, a common trope in subscription applications is to offer the
cheapest plan for one user, a more expensive plan for a few more users, and a much more
expensive plan for many users.

In this application, we've got just one plan that all users will subscribe to for the same price, so
the view only needs to say something to the effect of "please subscribe to use this app":

60
Chapter 8: Signing users up for a monthly subscription plan

Views/Subscription/Start.cshtml

@model SubscribeViewModel
@{
ViewData["Title"] = "Subscribe";
}

<div class="text-center">
<h2>Activate Subscription</h2>
<hr/>
</div>

<form method="post" asp-controller="Subscription" asp-


action="HandleStartSubscription">
<p>Thanks for installing this Shopify app! You must accept a recurring monthly
subscription to continue.</p>
@if (Model.ShowError)
{
<p class="red error">@Model.Error</p>
}
<button type="submit" class="btn btn-primary">
Subscribe
</button>
</form>

With those two views and viewmodels out of the way, we can move on to the controller itself.
Create a new C# class file named SubscriptionController.cs in your Controllers folder. This new
controller needs to be decorated with the Authorize attribute, as users should not be able to
access any of the actions here unless they're already logged in; the attribute will ensure that they
are.

As for Dependency Injection services, the new controller will need access to the UserContext ,
ISecrets and IApplicationUrls services from Dependency Injection. Add those via the

constructor along with the following using statements:

Controllers/SubscriptionController.cs

using System;
using System.Threading.Tasks;
using AuntieDot.Attributes;
using AuntieDot.Data;
using AuntieDot.Extensions;
using AuntieDot.Models;
using Microsoft.AspNetCore.Authentication;
using ShopifySharp;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

61
Chapter 8: Signing users up for a monthly subscription plan

using System.Linq;
using ShopifySharp.Filters;

namespace AuntieDot.Controllers
{
[Authorize]
public class SubscriptionController : Controller
{
private readonly UserContext _userContext;
private readonly ISecrets _secrets;
private readonly IApplicationUrls _appUrls;

public SubscriptionController(UserContext userDb, ISecrets secrets,


IApplicationUrls urls)
{
_userContext = userDb;
_secrets = secrets;
_appUrls = urls;
}
}
}

While the controller is only going to do two things (subscribing a user to a monthly plan, and
showing details about their subscription), it will take four actions to accomplish that:

1. The Index action, which renders the Views/Subscription/Index.cshtml view displaying the
user's subscription details.
2. The Start action, which renders the Views/Subscription/Start.cshtml view asking the user
to subscribe before using the app.
3. The HandleStartSubscription action, where the form request is sent after users press the
form's "Subscribe" button. This one is responsible for creating a new subscription charge
and redirecting the user to Shopify where they'll accept or decline the charge.
4. The ChargeResult action, where Shopify returns the user after they accept the charge. This
one will be responsible for activating the subscription charge, which will either start the free
trial period or immediately charge the user if the charge has no free trial period.

We can being with the Index action, where the app will need to pull in the user's subscription
charge details via the Shopify API. This one should be decorated with the
AuthorizeWithActiveSubscription attribute (the one we wrote in an earlier chapter), because

we only want users who are already subscribed to view this page.

However, despite decorating the action with that attribute, we should be extra careful here. It's
possible for the user's account model to become out of sync with their session during a brief
period of time where they've uninstalled the app but their session hasn't yet expired.

62
Chapter 8: Signing users up for a monthly subscription plan

Admittedly the user is unlikely to be using the app after uninstalling it, but since the action needs
to pull in the user model from the database anyway to use the access token, it can also check that
the user still has a valid Shopify charge ID. The ID will be set to null when the "AppUninstalled"
webhook is sent by Shopify (which we'll set up in a later chapter), so we can trust that if it's null
here the user isn't currently subscribed despite being able to get past the
AuthorizeWithActiveSubscription attribute:

Controllers/ShopifyController.cs

// ...

public class ShopifyController : Controller


{
// ...

[AuthorizeWithActiveSubscription]
public async Task<IActionResult> Index()
{
var userSession = HttpContext.User.GetUserSession();
var user = await _userContext.Users.SingleAsync(u => u.Id ==
userSession.UserId);

if (user.ShopifyChargeId.HasValue == false)
{
return RedirectToAction("Start");
}

// TODO: pull in the subscription charge from Shopify's API


}
}

When pulling in the user's subscription charge from the Shopify API, we'll want to be careful
about situations where Shopify's API might return an HTTP 404 Not Found result. There are a
few reasons why Shopify might return a 404 when looking up the charge, including things like a
failed credit card payment from the store owner; the user's Shopify account was frozen, deleted or
paused, or they've simply canceled their subscription to your app by uninstalling it.

That last one will be an extremely rare case where the user uninstalled the app while signed in,
Shopify either failed to send the App Uninstalled webhook in a timely manner or the app failed to
handle it correctly, and the user tried to access the subscription details page before their session
expired. That's probably never going to happen, but we can easily guard against all of these
situations by wrapping the chargeService.GetAsync call in a try/catch and testing for the 404
status code when an exception is thrown:

Controllers/ShopifyController.cs

63
Chapter 8: Signing users up for a monthly subscription plan

// ...

public class ShopifyController : Controller


{
// ...

[AuthorizeWithActiveSubscription]
public async Task<IActionResult> Index()
{
// ...

// Pull in the user's subscription charge from Shopify


var chargeService = new RecurringChargeService(user.ShopifyShopDomain,
user.ShopifyAccessToken);
RecurringCharge charge;

try
{
charge = await chargeService.GetAsync(user.ShopifyChargeId.Value);
}
catch (ShopifyException e) when (e.HttpStatusCode ==
HttpStatusCode.NotFound)
{
// The user's subscription no longer exists. Update their user model
to delete the charge ID
user.ShopifyChargeId = null;

await _userContext.SaveChangesAsync();

// Update the user's session, then redirect them to the subscription


page to accept a new charge
await HttpContext.SignInAsync(user);

return RedirectToAction("Start");
}

return View(new SubscriptionViewModel(charge));


}
}

Any time you want to get one single object from the Shopify API (e.g. the an order, a shop's
details, a user's subscription), you can wrap the call in a try/catch block and catch HTTP 404
Not Found exceptions. When these are thrown, it means the object was either deleted or
never existed in the first place.

Voila, now if a user is ever curious about the details of their subscription, they'll be able to visit
"/Subscription" to get the lowdown.

64
Chapter 8: Signing users up for a monthly subscription plan

The next action is the Start action, which just needs to return the view asking the user to start
their subscription. However, we probably don't want users who are already subscribed to try
starting a new subscription, otherwise savvy users could technically give themselves free trials
over and over, starting a new subscription every time their current one gets near the end.

To guard against that, we just need to check their session and ensure the IsSubscribed property
is false. If it's false, the view can be returned with an empty SubscribeViewModel (since there's
no error message we need to show the user at this point):

Controllers/SubscriptionController.cs

// ...

public class SubscriptionController : Controller


{
// ...

[HttpGet]
public async Task<IActionResult> Start()
{
// Make sure the user isn't already subscribed
var userSession = HttpContext.User.GetUserSession();

if (userSession.IsSubscribed)
{
return RedirectToAction("Index", "Home");
}

return View(new SubscribeViewModel());


}
}

When the Start form gets submitted, its data will be sent to the next action:
HandleStartSubscription . Since the form is submitted as a "POST" request, the action will

need to be decorated with an HttpPost attribute, which tells ASP.NET to only route requests to
this action if they're POST requests.

Just like the last action, we'll want to double check that the user isn't already subscribed, but this
time we want to be double sure by checking the database -- just in case their current session isn't
up-to-date. For example, perhaps the user had opened the subscription form in two different tabs
or two different browsers, and then subscribed in one of them. Their session in the previous
browser would not have been updated, and if we only checked the session they'd be able to get
through and start their free trial over from the beginning.

65
Chapter 8: Signing users up for a monthly subscription plan

If the app finds that is the case, then it should update their current session with the latest user
model before redirecting them to the home page. Without updating their session, it's possible that
the user could get stuck in a redirect loop: their session says they're not subscribed, so the app
sends them to the subscription page, but their database model says they are subscribed, so the app
sends them to the home page.

Controllers/SubscriptionController.cs

// ...

public class SubscriptionController : Controller


{
// ...

[HttpPost]
public async Task<IActionResult> HandleStartSubscription()
{
// Make sure the user isn't already subscribed
var userSession = HttpContext.User.GetUserSession();
var user = await _userContext.Users.FirstOrAsync(u => u.Id ==
userSession.UserId);

if (user.ShopifyChargeId.HasValue)
{
// Update the user's session to prevent redirect loops
await HttpContext.SignInAsync(user);

// Send them to the home page


return RedirectToAction("Index", "Home");
}

// TODO: create a new subscription and send the user to the confirmation
url
}
}

And now at this point we can configure the recurring subscription charge, which is represented in
code by ShopifySharp's RecurringCharge object. There are a few quirks with subscription
charges, and some of those quirks can come back to bite you in the butt if you're not careful.
Here are the things that you really need to know about Shopify's recurring charges before
you start using them:

1. Your Shopify application can only have one recurring charge per shop. If you try to create a
new recurring charge for a user who already has one, you're just going to replace their
existing charge with the newer one.
2. As mentioned earlier in this chapter, a charge does not actually start charging until you

66
Chapter 8: Signing users up for a monthly subscription plan

activate it with the API. Accordingly, the free trial does not start until the charge has been
activated either.
3. A charge cannot be activated until the user is sent to the charge's confirmation URL to
accept the charge.
4. Once accepted by the user, the RecurringCharge.Status property will be Accepted .
Trying to activate a charge with any other status will throw an exception.
5. There's no way to update a recurring charge to change its price, name or free trial length. If
you want to change any of those things, you'll have to create a brand new charge with the
properties you want, and then get the user to accept it.
6. Shopify will delete any charges with a Pending status -- one that has been neither accepted
or declined -- about 48 hours after its created.
7. Charges can be created in "test" mode, which is specifically for us developers to use when
developing applications. A charge created in test mode will go through a free trial and
billing cycle like normal, but you won't be charged real money at any point.

So with those things in mind, it's time to create the recurring subscription charge and send the
user to the confirmation URL to accept or decline it. This is another place where you might
want to customize the code that follows. For this guide, we're going to subscribe the user to a
monthly plan with a price of $9.99 per month. We're also going to give the user a free trial period
of seven days, which means they'll get to try out the app for a week before being billed. The user
can uninstall the app at any point before those seven days are up, and they won't be billed.

Trial periods are optional, and you can leave the value null or set it to 0 if you want to
charge the user as soon as the subscription is activated.

Controllers/SubscriptionController.cs

// ...

public class SubscriptionController : Controller


{
// ...

[HttpPost]
public async Task<IActionResult> HandleStartSubscription()
{
// ...

var service = new RecurringChargeService(user.ShopifyShopDomain,


user.ShopifyAccessToken);
var charge = await service.CreateAsync(new RecurringCharge
{
TrialDays = 7,
Name = "My App's Subscription Plan",
Price = 9.99M,

67
Chapter 8: Signing users up for a monthly subscription plan

ReturnUrl = _urls.SubscriptionRedirectUrl,
// If the app is running in development mode, make this a test charge
Test = _environment.IsDevelopment()
});

// Send the user to the charge's confirmation URL


return Redirect(charge.ConfirmationUrl);
}
}

After the charge is created and the user is sent to the confirmation URL, they'll see a screen that
looks something like this:

At that point, we cross our fingers and hope they accept! Assuming they do, the user is sent back
to the redirect URL that was passed in when creating the charge: /Subscription/ChargeResult .
That request will be handled by the SubscriptionController.ChargeResult action, which we'll
implement now.

When the user gets back to the app, Shopify will have attached two values in the querystring:
shop and charge_id . The shop value can be ignored because the user will be logged in for

this action (and we can guarantee that because the controller itself is decorated with the
Authorize attribute). The charge_id value will be used to pull in the user's recurring charge

data from the Shopify API.

Before doing that though, it's still a good idea to get the user's record from the database to once
again check if they're already subscribed. This protects against cases where the user opened
multiple tabs to start a subscription (for whatever reason), and then accepted a subscription

68
Chapter 8: Signing users up for a monthly subscription plan

charge more the once.

Controllers/SubscriptionController.cs

// ...

public class SubscriptionController : Controller


{
// ...

[HttpPost]
public async Task<IActionResult> ChargeResult([FromQuery] string shop,
[FromQuery] long charge_id)
{
// Grab the user from the database
var userSession = HttpContext.Users.GetUserSession();
var user = await _userContext.Users.FirstOrAsync(u => u.Id ==
userSession.UserId);

if (user.ShopifyChargeId.HasValue)
{
// The user has already subscribed
return RedirectToAction("Index", "Home");
}

// TODO: get the charge via Shopify API and check its status
}
}

The most important part of the subscription process comes now, where we need to pull in the
recurring change and check its status. Unfortunately you can't assume that the user has subscribed
just because Shopify sent them to your redirect URL, as Shopify will also send them back to the
URL if the decline the charge. Presumably this is so you can show some kind of error message to
the user, asking them to reconsider their choice.

Whatever the case, the app needs to check the status of the charge here before trying to activate it,
as activating a charge whose status is not accepted will throw an error. There are several
different charge statuses we'll want to check for, and what the action does next varies based on
the value:

1. If the status is pending , the user needs to be sent back to the confirmation URL as they
haven't yet accepted or declined the charge.
2. If the status is expired or declined , the user will need to be sent back to the
SubscriptionController.Start action to start the process over. A charge will

automatically expire after 48 hours if the user doesn't accept or decline it in that time frame.
3. If the status is active , the user has already accepted the charge, and this very action has

69
Chapter 8: Signing users up for a monthly subscription plan

already activated it. Here we'd want to update the user's account data with the charge ID
(since, to get to this point, something must have gone wrong and their account was not
updated with the charge ID when it was activated).
4. Finally, if the status is accepted , the user has accepted the charge and it needs to be
activated using ShopifySharp. The free trial and the payment do not begin until the charge
has been activated.

Keep in mind that all of these statuses are case-sensitive!

Controllers/SubscriptionController.cs

// ...

public class SubscriptionController : Controller


{
// ...

[HttpPost]
public async Task<IActionResult> ChargeResult([FromQuery] string shop,
[FromQuery] long charge_id)
{
// ...

// Get the charge via Shopify API and check its status
var service = new RecurringChargeService(user.ShopifyShopDomain,
user.ShopifyAccessToken);
var charge = await service.GetAsync(charge_id);

switch (charge.Status)
{
case "pending":
// User has not accepted or declined the charge
return Redirect(charge.ConfirmationUrl);

case "expired":
case "declined":
// Send the user back to start a subscription again
return RedirectToAction("Start");

case "active":
// User has already accepted this charge and activated it.
// Update their account and session, then send them to the home
page
user.ShopifyChargeId = charge_id;
await _userContext.SaveChangedAsync();
await HttpContext.SignInAsync(user);

return RedirectToAction("Index", "Home");

70
Chapter 8: Signing users up for a monthly subscription plan

case "accepted":
// User has accepted the charge, and it can now be activated
user.ShopifyChargeId = charge_id;
await service.ActivateAsync(charge_id);
await _userContext.SaveChangedAsync();
await HttpContext.SignInAsync(user);

return RedirectToAction("Index", "Home");

default:
var message = $"Unhandled charge status of {charge.Status}";
throw new ArgumentOutOfRangeException(nameof(charge.Status),
message);
}
}
}

Note: in an upcoming version of their API, Shopify will no longer require applications to
activate a recurring charge. Instead the charge will be activated automatically once the user
accepts it. This book will be updated once that happens, but the only thing that will need to
be updated above is removing the accepted case entirely. Instead you'll just update the
user's subscription ID if the charge's status is active .

And that's it! Once the user accepts the charge and hits the
SubscriptionController.ChargeResult action, their subscription will start and they'll be

charged either immediately or at the end of their free trial. If they uninstall the application at any
point before their free trial ends, they will not be charged and you will not receive a payment for
their subscription.

In an upcoming chapter, we'll set up webhook listeners which will tell the application when a user
has uninstalled the app so their database record can be updated. But next up, we're going to build
out the Home controller, which will use the Shopify API to show users a list of every order that
has been placed on their Shopify store.

71
Chapter 9: Using the Shopify API to load a list of Shopify orders

Using the Shopify API to load a list of Shopify


orders
We're in the homestretch now! Users can install and log in to our app; they can start a monthly
subscription with a free trial; and they can view the details of that subscription whenever they
want. At this point, you've got the basic framework for a working Shopify application, and you
can start working on the features that will make your app unique.

For this base AuntieDot application, we're going to keep the features fairly simple -- we just want
to use the Shopify Orders API to display a list of a user's Shopify orders. As usual, let's start with
this controller's view models, and the views that use them. We're only going to have one action
for this whole controller, but we'll need two new model classes to hold the data necessary for
displaying a list of orders to the user.

In your Models folder, create a new C# class file named OrderSummary.cs. This class is just
going to be a pared-down version of ShopifySharp's own Order class. It'll contain only the
properties that we care about showing to the user, instead of the dozens of properties provided by
ShopifySharp. The ones we care about are the following:

1. OrderId : the order object's Shopify API ID. This is the ID you'll use as a developer when

you want to get, update or delete an order.


2. Name : the "name" of the order, which is more like its "human ID". For example, "#1001".

This is the ID that a merchant would see in their Shopify dashboard, but it is not the
order's API ID and cannot be used with the API.
3. DateCreated : the date the order was created.

4. CustomerName : the name of the customer who bought the order. Shopify's Customer

objects have a first name and a last name, but our summary class will combine them into one
full name.
5. LineItemSummary : a summary of the items in the order, e.g. "2 x Bath Towel and 3 other

items."

In your Models folder, create a new C# class file named OrderSummary.cs. Just like the
SubscriptionViewModel class, it's going to receive another object -- this time a

ShopifySharp.Order -- in the constructor, from which it will assign its own property values.

Special care will be taken when assigning the customer name (because the Customer property
on a Shopify order can be null if the order was placed via the API), along with the line item
summary (because not all orders will have line items). Additionally, we want to change the text

72
Chapter 9: Using the Shopify API to load a list of Shopify orders

of the line item summary based on how many items are in the order:

Models/OrderSummary.cs

using System.Linq;

namespace AuntieDot.Models
{
public class OrderSummary
{
public OrderSummary(ShopifySharp.Order order)
{
OrderId = order.Id.Value;
Name = order.Name;
DateCreated = order.CreatedAt.Value.ToString();

if (order.Customer != null)
{
CustomerName = $"{order.Customer.FirstName}
{order.Customer.LastName}";
}
else
{
CustomerName = "(No customer)";
}

var totalLineItems = order.LineItems.Count();

if (totalLineItems == 0)
{
LineItemSummary = "No line items, order is empty.";
}
else if (totalLineItems == 1)
{
var li = order.LineItems.First();

LineItemSummary = $"{li.Quantity} x {li.Title}";


}
else
{
var li = order.LineItems.First();
var totalOtherItems = order.LineItems.Sum(item => item.Quantity) -
li.Quantity;

LineItemSummary = $"{li.Quantity} x {li.Title} and


{totalOtherItems} other items.";
}
}

public long OrderId { get; set; }

73
Chapter 9: Using the Shopify API to load a list of Shopify orders

public string Name { get; set; }

public string DateCreated { get; set; }

public string CustomerName { get; set; }

public string LineItemSummary { get; set; }


}
}

Now, when the new controller pulls in a list of Shopify orders, it can easily map them to this new
summary class without having to add the property configuration code to the controller itself.

For the second model, create a new class file named HomeViewModel.cs in your Models folder,
and add three properties to it: the list of order summaries, a NextPage string, and a
PreviousPage string, both of which will be required when the user wants to go from one page to

the next/previous page.

Models/HomeViewModel.cs

using System.Collections.Generic;

namespace AuntieDot.Models
{
public class HomeViewModel
{
public IEnumerable<OrderSummary> Orders { get; set; }

public string NextPage { get; set; }

public string PreviousPage { get; set; }


}
}

And the last step, before we move on to the controller itself, is to write the home page view. Like
the _Layout.cshtml file that we wrote a few chapters ago, this one is going to be split into two
different parts: the view itself, and a partial view that renders the Next Page and Previous Page
links. Splitting the view is not strictly necessary; it's only being done here because we want to
add the page links in two different places, so moving them to their own view means we can just
use it twice instead of writing the same code twice.

In your Views/Home folder, create a new file named _PageLinks.cshtml. It's going to use the
HomeViewModel model, and will check to see if the model contains previous page or next page

links. Thanks to Shopify's API, the next and previous pages of orders can only be visited using
the link values returned by ShopifySharp.

74
Chapter 9: Using the Shopify API to load a list of Shopify orders

We'll dive into this quirk more in depth in just a moment, once we get to the controller code, but
to summarize: if you don't have the special, randomly-generated "keys" to the next page or
previous page, Shopify's API will always return the first page of orders instead. We add those
keys to the querystring when the user tries to visit the next page or previous page, and the
controller is going to take them to use when requesting the orders from Shopify.

Views/Home/_PageLinks.cshtml

@model HomeViewModel

<div class="row">
<div class="col-sm-6 text-sm-left">
@if (Model.PreviousPage != null)
{
<a class="btn-link" asp-controller="Home" asp-action="Index" asp-
route-pageInfo="@Model.PreviousPage">
Previous Page
</a>
}
</div>
<div class="col-sm-6 text-sm-right">
@if (Model.NextPage != null)
{
<a class="btn-link" asp-controller="Home" asp-action="Index" asp-
route-pageInfo="@Model.NextPage">
Next Page
</a>
}
</div>
</div>

This partial view contains another Razor tag that we haven't seen yet. What is asp-route-
pageInfo ? When we build out the HomeController.Index action, the action method is going to

have a string parameter named pageInfo that we specifically get from the request's querystring.
The Razor tag attribute is going to automatically add the value to the querystring when the page
is rendered, so again it's just another fancy Microsoft-ism that will give you extra intellisense and
warnings in your IDE (don't be surprised if your IDE is giving you an error about it write now --
we haven't written the controller or action code that it's linking to yet).

As always, this is not at all necessary and also not at all standard HTML. You could just as easily
replace it with this:

Example

<!-- An example using standard HTMl instead of ASP.NET's non-standard Razor route
tags -->

75
Chapter 9: Using the Shopify API to load a list of Shopify orders

<a class="btn-link" href="/Home/Index?pageInfo=@Model.PageInfo">


Next Page
</a>

Now in the Home view itself, we just need to use @await Html.PartialAsync("_PageLinks",
Model) to render the Next Page / Previous Page links where we want them.

The Home view's purpose is to display a table of the user's Shopify orders. We want to display
the order name (again, this is not the order's API ID, that's different); the date the order was
created; the customer's name; and a summary of the line items in the order. As we configured
above, the OrderSummary class will already have all of those properties mapped out for us, so it's
just a matter of rendering them in the view.

In your Views/Home folder, create a new view file named Home.cshtml. This one receives the
HomeViewModel class as its model. The home view will use an HTML table and the @foreach

Razor loop syntax to loop over all of the orders in the model, adding a new HTML table row for
each order.

Views/Home/Index.cshtml

@model HomeViewModel
@{
ViewData["Title"] = "Orders";
}

<div class="text-center">
<h2>Your Orders</h2>
<hr/>
</div>
@await Html.PartialAsync("_PageLinks", Model)
<table class="table table-bordered table-striped">
<thead>
<tr>
<td>Order #</td>
<td>Date Created</td>
<td>Customer Name</td>
<td>Line Item Summary</td>
</tr>
</thead>
<tbody>
@foreach (var order in Model.Orders)
{
<tr>
<td>@order.Name</td>
<td>@order.DateCreated</td>
<td>@order.CustomerName</td>
<td>@order.LineItemSummary</td>

76
Chapter 9: Using the Shopify API to load a list of Shopify orders

</tr>
}
</tbody>
</table>
@await Html.PartialAsync("_PageLinks", Model)

And there we have it! We're now ready to move on to the controller itself, which will pull in the
orders from the Shopify API and pass them directly to the view using the model class.

In your Controllers folder, create a new C# class file named HomeController.cs (if it doesn't
already exist -- if it does, clear out the contents of the file), then add the following

Let's start with creating a new controller class, adding the using statements and properties we'll
need.

In your project directory, create a new C# controller class file named HomeController.cs (if it
doesn't exist already -- if it does, clear out the contents of the file). This controller only needs two
items from Dependency Injection: an ILogger instance scoped to the controller (so we can log
messages), and the database context.

Add those DI services to your new controller, along with the following using statements:

Controllers/HomeController.cs

using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using AuntieDot.Attributes;
using AuntieDot.Data;
using AuntieDot.Extensions;
using AuntieDot.Models;
using ShopifySharp;
using ShopifySharp.Filters;

namespace AuntieDot.Controllers
{
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;

private readonly Usercontext _userContext;

public HomeController(ILogger<HomeController> logger, UserContext

77
Chapter 9: Using the Shopify API to load a list of Shopify orders

userContext)
{
_logger = logger;
_userContext = userContext;
}
}
}

Really quickly, while the Home controller is mainly for working with the order's we've spent all
chapter talking about, we can't forget to implement an Error action! This one is used by the
ASP.NET Framework whenever the application encounters some kind of unexpected exception.
It will send the user to this "/Home/Error" route instead of throwing a big stack trace and
technical error message at them -- just as we had configured in the Startup.cs file with the
app.UseExceptionHandler("/Home/Error") line, back in Chapter 6.

The Error action is decorated with a [ResponseCache] attribute that we can use to ensure the
page never gets cached by the browser (meaning the user's browser will never accidentally cache
an older, irrelevant error message). All the action needs to do is figure out some kind of identifier
for the user's request, which is already built in to the ASP.NET Framework, and then return the
Views/Home/Error.cshtml view that we wrote in Chapter 6:

Controllers/HomeController.cs

public class HomeController : Controller


{
// ...

[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore =


true)]
public IActionResult Error()
{
var userRequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;

return View(new ErrorViewModel


{
RequestId = userRequestId
});
}
}

Now let's get to the fun part! We want the Index action to load a list of the user's orders from
their Shopify store using the Shopify API. This is thankfully an easy thing to do thanks to
ShopifySharp's OrderService class, which as a dedicated ListAsync function. However, there
are some limitations built into Shopify's API that you'll need to know about when it comes to
listing almost any object -- not just orders:

78
Chapter 9: Using the Shopify API to load a list of Shopify orders

1. You can only list up to 250 items per API call. That means if your user has more than 250
orders, you've got to make extra calls to the Shopify API.
2. While you were once able to arbitrarily specify a page of orders to load (e.g. "load page
35"), you can't do this any longer. Instead, each time you load a page the Shopify API will
return a "link" to the next page and the previous page, and only those links can be used to
load the next or previous page.
3. Shopify has a rate limit on almost all API requests. You're allowed only two API calls per
second, with a burst of up to 40 calls per second, before the API returns a rate limit error
asking you to wait before making more calls.

These API limitations put certain design limitations on our application. For example, it's not
possible to let users arbitrarily skip from page 1 of their orders to page 10. The user must first
request page 1, then page 2, then page 3, and so on. Shopify has turned the entire list of orders
into a linked list of lists. Page 1 is linked to Page 2, Page 2 is linked to Page 1 and Page 3, etc.
The only way to get the page you want (e.g. page 10), is to walk through those links until you get
there.

ShopifySharp will help you do this by returning a ListResult<Order> (for example), which has
four properties on it:

ShopifySharp's ListResult class

public class ListResult<Order>


{
public IEnumerable<Order> Items { get; }

public LinkHeaderParseResult<Order> LinkHeader { get; }

public bool HasNextPage { get; }

public bool HasPreviousPage { get; }


}

So, there's a list of orders here, along with something called a "LinkHeader", and two booleans
telling you whether the list has a next page and a previous page. There are also two helper
methods on the list result which you can use to get an object for listing the next or previous
pages. They're called GetPreviousPageFilter() and GetNextPageFilter() . All they're doing
behind the scenes though is looking at that LinkHeader object to see if it's null and has links to
the pages:

ShopifySharp's ListResult methods

public class ListResult<Order>


{

79
Chapter 9: Using the Shopify API to load a list of Shopify orders

// ...

public LinkHeaderParseResult<Order> LinkHeader { get; }

public ListFilter<Order> GetNextPageFilter(int? limit = null, string fields =


null)
{
return LinkHeader?.NextLink?.GetFollowingPageFilter(limit, fields);
}

public ListFilter<Order> GetPreviousPageFilter(int? limit = null, string


fields = null)
{
return LinkHeader?.PreviousLink?.GetFollowingPageFilter(limit, fields);
}
}

We'll be using those methods in this controller, as the object they return (a ListFilter<Order> )
has a property on it called PageInfo -- a plain old string, which is the key to listing pages.
Technically you don't need all these fancy objects like filters and results; all Shopify wants when
listing past page one is this string.

We just need to get the string, add it to the view model, and have the view add it to the
querystring when the user clicks the "next page" or "previous page" buttons. Then we'll
reconstruct the ListFilter<Order> object using the string, and ShopifySharp will take care of
the rest.

Now, if you really want to let your users skip around to arbitrary pages instead of going one by
one, there's only one way to get around this limitation: import the orders from Shopify and store
them in your own database. That way you've got access to all of the orders, and you can write
your own database queries to return an arbitrary page of them. You'd also want to set up an
"order/created" webhook to have Shopify send your application each new order as they come in.

We're not going to do that for this base AuntieDot project, but what follows is an example for
using ShopifySharp to loop through all of the orders on a Shopify store and then store them in a
list. You would then presumably pass the list to your database to save permanently:

Example

public async Task ListAllOrdersOnShop()


{
var executionPolicy = new ShopifySharp.SmartRetryExecutionPolicy();
var service = new OrderService(shopDomain, accesstoken, executionPolicy);
var allOrders = new List<ShopifySharp.Order>();
var page = await service.ListAsync(new ShopifySharp.OrderListFilter
{

80
Chapter 9: Using the Shopify API to load a list of Shopify orders

Limit = 250
});

// Keep adding the orders to the list of all orders until there are no further
pages to list
while (true)
{
allOrders.AddRange(page.Items);

if (!page.HasNextPage)
{
// We've reached the end of the list
break;
}

// There is at least one more page, list it and loop again


page = await service.ListAsync(page.GetNextPageFilter());
}

// TODO: do something with the `allOrders` variable


}

Note that in this example, ShopifySharp has been configured to use a


SmartRetryExecutionPolicy , which is not the default execution policy. This smart policy

will try to pace out the requests your app is making to the Shopify API in an effort to avoid
the rate limit. It will also handle cases where it accidentally goes over the rate limit, and will
retry after a brief pause.

By its nature, using this request policy can slow down your requests as they're essentially
being throttled by the policy. However, if you don't use this policy or the
RetryExecutionPolicy (which will blindly retry requests but not try to space them out),

then the default behavior is to throw a rate limit exception as soon as the application hits the
rate limit.

Let's start off the Index action by decorating it with an [AuthorizeWithActiveSubscription]


attribute, to ensure only subscribed users can reach the page. We also need to pull out the
pageInfo value from the querystring which will contain the link value for the next or previous

page. It'll default to null , which will let this action handle both page one and all of the pages
beyond that.

Controllers/HomeController.cs

public class HomeController : Controller


{
// ...

81
Chapter 9: Using the Shopify API to load a list of Shopify orders

[AuthorizeWithActiveSubscription]
public async Task<IActionResult> Index([FromQuery] string pageInfo = null)
{
// TODO: build a ShopifySharp list filter and list the requested page of
orders
}
}

The first thing the action needs to do is pull the user record out of the database, so we can use
their access token. After that, we need to build a ShopifySharp ListFilter which contains all
of the necessary configuration to list a certain page of orders. The filter has three properties on it
that are relevant to us:

1. PageInfo : that special string that Shopify requires for listing pages beyond page one. It can

be null, in which case Shopify will return the first page; that means we don't need any
special handling for our pageInfo querystring value -- we just pass it straight to
ShopifySharp and let the Shopify API sort out which page the user wants.
2. Limit : tells Shopify how many orders we want to list at a time. The value can be anywhere

between 1 to 250. The default is 50.


3. Fields : tells Shopify which of the fields on the object we want returned. If you specify

fields, Shopify will only return those fields on your objects (orders in our case). This helps
speed up the request and saves bandwidth, so depending on your use case it can be a good
idea to specify the fields. Note: the fields must be comma separated, and they must use
their JSON name -- not their C# property name. For example, if you want to get just the id
and the name, you'd pass in id,name instead of Id,Name .

In this base application, the only order fields we need are the name, ID, customer, line items and
the date the order was created. We'll also limit the request to only return 25 orders per page,
although this is another place where you can customize the application to return more or less
orders, or even let the user decide how many orders should be returned per page.

Once we've got the list filter configured, we pass it to ShopifySharp to pull in the orders, and then
map the orders to our new OrderSummary class. After the summaries are mapped, we configure
the HomeViewModel by passing in the next/previous page links and the summaries, and then
render the view!

Controllers/HomeController.cs

public class HomeController : Controller


{
// ...

[AuthorizeWithActiveSubscription]
public async Task<IActionResult> Index([FromQuery] string pageInfo = null)

82
Chapter 9: Using the Shopify API to load a list of Shopify orders

{
var userSession = HttpContext.User.GetUserSession();
var user = await _userContext.Users.FirstAsync(u => u.Id ==
userSession.UserId);
var service = new OrderService(user.ShopifyShopDomain,
user.ShopifyAccessToken);

// Build a list filter to get the requested page of orders


var limit = 25;
// Only get the fields we'll use in the OrderSummary model
var orderFields = "name,id,customer,line_items,created_at";
var filter = new Listfilter<Order>(pageInfo, limit, orderFields);
var orders = await service.ListAsync(filter);

return View(new HomeViewModel


{
Orders = orders.Items.Select(o => new OrderSummary(o)),
NextPage = orders.GetNextPageFilter()?.PageInfo,
PreviousPage = orders.GetPreviousPageFilter()?.PageInfo
});
}
}

And that's it! Your home page now loads and renders a list of Shopify orders which are pulled
from the user's store. We'll take a quick walk through the application to make sure everything is
functioning as expected, but there's one last thing we need to cover before the base AuntieDot
project is done: how to handle and validate Shopify's webhooks.

83
Chapter 10: Validating and handling Shopify's webhooks

Validating and handling Shopify's webhooks


Webhooks are a wonderfully useful technology, but they can be a little confusing if you've never
dealt with them before. The benefit that you get from using them, though, is immense --
especially if you're building or running a Shopify application for the Shopify app store. As
discussed briefly back when we were setting up the ShopifyController class, a webhook is a
special kind of contract between you and Shopify that effectively tells Shopify "Hey, when this
thing happens on a user's store, tell my app about it by making a request to this URL".

As we briefly discussed back when we were setting up the ShopifyController class and
registering the "app/uninstalled" webhook, there are a ton of different events that you can create a
webhook for: when an order has been created; when a customer's profile has been changed; when
an order has been fulfilled; when a user's shop has been updated (e.g. they changed the name or
contact email address); etc.

When you register a webhook for one of these events, Shopify will send a POST request to the
URL you give it every time the event occurs. In ASP.NET Core terms, you can think of a
webhook as a simple request hitting a controller's action. Except this controller and action are
dedicated to webhooks and they shouldn't be requested by actual users.

There are two kinds of webhooks in the world:

The firehose webhook: You give the service a single URL, and they'll hit that URL with
every single event that occurs. The request's JSON payload will contain some kind of "event
type" property that you use to determine what happened and what you want to do with the
data. Stripe, a payment processor that you could once use with Shopify some years ago, is a
great example of the firehose webhook.
The fine-grained webhook: You set up multiple webhook URLs, and each URL is only
used for that specific type of event. Instead of reading the JSON payload to determine the
event type, you know which event occurred and what kind of data is in the payload by the
URL that was used.

As you've probably already guessed, Shopify's webhooks are of the fine-grained variety. We've
already created an "app/uninstalled" webhook after the user installs the app and we grab their
access token, and we know what has happened when our app receives a POST request the
"/webhooks/app-uninstalled" URL.

84
Chapter 10: Validating and handling Shopify's webhooks

No matter what the event topic is or which URL the data gets sent to, it's very important that the
request gets validated using Shopify's webhook validation scheme. If you don't validate your
webhooks, it would be possible for attackers to send their own fake events to your endpoints. For
example, by the end of this chapter the "app/uninstalled" webhook handler is going to delete a
user's Shopify access token; if an attacker figures out the endpoint for that handler, they could
potentially delete the access tokens for a user, breaking the app from the user's point of view.

Thankfully validating a webhook request is almost as simple as validating the requests we already
get from Shopify during the OAuth installation/login process. The method of validation is just
slightly different, which means we'll need a new validation attribute.

In your project's Attributes folder, create a new C# class file named


ValidateShopifyWebhookAttribute.cs. Just like the other request validation attribute, this one
needs to extend the ActionFilterAttribute class and override the OnActionExecutionAsync
method:

Attributes/ValidateShopifyWebhookAttribute.cs

using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using AuntieDot.Models;
using AuntieDot.Extensions;
using ShopifySharp;
using System.IO;
using System.Text;
using Newtonsoft.Json;

namespace AuntieDot.Attributes
{
public class ValidateShopifyWebhookAttribute : ActionFilterAttribute
{
public override async Task OnActionExecutionAsync(
ActionExecutingContext context,
ActionExecutionDelegate next)
{
// TODO: validate the webhook request
}
}
}

If you remember several chapters ago where we wrote the original


ValidateShopifyRequestAttribute , Shopify provided a signature value which was the result

of combining all the other values in the querystring and then hashing them with your app's secret

85
Chapter 10: Validating and handling Shopify's webhooks

key. We knew a request was valid if we hash those values ourselves and then compare the
signatures to see if they're equal. This is what ShopifySharp does behind the scenes with its
AuthorizationService.IsAuthenticRequest method.

Validating a webhook request is much the same process, except instead of hashing values in a
querystring, Shopify is hashing the entire HTTP request body with your secret key. This time
they'll add the signature to the HTTP request headers instead of the querystring, so to validate the
request all you'd need to do is grab that signature header (named X-Shopify-Hmac-SHA256 ) and
then hash the request body yourself to compare the signatures.

This is how ShopifySharp does it with its AuthorizationService.IsAuthenticWebhook method:

Example: How ShopifySharp validates webhook requests

public static bool IsAuthenticWebhook(


HttpRequestHeaders requestHeaders,
string requestyBody,
string shopifySecretKey)
{
// Find the signature key
var headerName = "X-Shopify-Hmac-SHA256";
var comparison = StringComparison.OrdinalIgnoreCase;
var hmacHeaderValue = requestHeaders
.FirstOrDefault(kvp => kvp.Key.Equals(headerName, comparison)
.Value
.FirstOrDefault();

if (string.IsNullOrEmpty(hmacHeaderValue))
{
// Couldn't find the signature header
return false;
}

// Compute a hash from the secret key and the request body
var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(shopifySecretKey));
var hash =
Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes(requestBody)));

// Webhook is valid if the hashes match


return hash == hmacHeader;
}

But because ShopifySharp already does that for us, we don't need to implement all of that code
ourselves! Rather our new attribute just needs to get the request headers, the request body, and
the secret Shopify API key, and then pass them all to ShopifySharp for validation.

86
Chapter 10: Validating and handling Shopify's webhooks

If the request is valid, we call that next() delegate to let the request continue on to the
controller. If it isn't valid, we set the response status code to 401 Unauthorized and return a JSON
error message:

Attributes/ValidateShopifyWebhookAttribute.cs

public override async Task OnActionExecutionAsync(


ActionExecutingContext context,
ActionExecutionDelegate next)
{
var rawBody = await context.HttpContext.Request.ReadRawBodyAsync();
// Get an instance of the ISecrets interface from Dependency Injection
var secrets = (ISecrets)
context.HttpContext.RequestServices.GetService(typeof(ISecrets));
var isAuthentic = AuthorizationService.IsAuthenticWebhook(
context.HttpContext.Request.Headers,
rawBody,
secrets.ShopifySecretKey);

if (isAuthentic)
{
// Request passed validation, let it continue on to the controller
await next();
}
else
{
// Request did not pass validation. Return a JSON error message
context.HttpContext.Response.ContentType = "application/json";

var body = JsonConvert.SerializeObject(new


{
message = "Webhook did not pass validation.",
ok = false
});

// Copy the JSON error message to the response body


using (var buffer = new MemoryStream(Encoding.UTF8.GetBytes(body)))
{
context.HttpContext.Response.StatusCode = 401;
await buffer.CopyToAsync(context.HttpContext.Response.Body);
}
}
}

And now when we decorate the webhook controller with this new [ValidateShopifyWebhook]
attribute, all requests will be automatically validated using Shopify's webhook validation scheme.
If the request does not pass validation, the attribute will return an error message saying so.

87
Chapter 10: Validating and handling Shopify's webhooks

Handling the "app/uninstalled" webhook


Let's get started on the new webhook controller. In your Controllers folder, create a new C# class
file named WebhookController.cs. Add the following using statements, and set up three DI
services via the class constructor: ISecrets , UserContext and a message logger with
ILogger<WebhooksController> . And don't forget to decorate the controller with the new

validation attribute!

Controllers/WebhooksController.cs

using ShopifySharp;
using System;
using System.Threading.Tasks;
using AuntieDot.Cache;
using AuntieDot.Data;
using AuntieDot.Models;
using AuntieDot.Extensions;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;

namespace AuntieDot.Controllers
{
[ValidateShopifyWebhook]
public class WebhooksController : Controller
{
public WebhooksController(
ISecrets secrets,
UserContext userContext,
ILogger<WebhooksController> logger)
{
_secrets = secrets;
_userContext = userContext;
_logger = logger;
}

private readonly ISecrets _secrets;


private readonly UserContext _userContext;
private readonly ILogger<WebhooksController> _logger;

}
}

Decorating the new controller with the [ValidateShopifyWebhook] attribute means that all
requests to any action on this controller, whether they're from Shopify or not, must pass the
validation test or be rejected. This means that any request to the webhook handlers we're about to

88
Chapter 10: Validating and handling Shopify's webhooks

write can be trusted and no extra validation or checking is needed.

The "app/uninstalled" webhook handler is actually fairly simple. All it needs to do is revert a
user's database model back to a state where the app will think they're unsubscribed and have not
connected to Shopify's OAuth; i.e. their ShopifyChargeId and ShopifyAccessTokens are both
null. That's very important, because by the time this webhook is fired, we know the following two
things:

1. The access token to the user's shop has already been invalidated and cannot be used to make
any API calls.
2. If the user was subscribed to the app with a monthly recurring plan, their subscription has
already been canceled and deleted.

Because the access token will be invalid at this point, any attempts to call the Shopify API will
fail and ShopifySharp itself will throw exceptions when you try to make an API call. That's why
we want to delete the access token and the charge from the database, to ensure the app doesn't
think they're connected or subscribed, and doesn't try to make API calls with an invalid token.
We also delete their ShopDomain and BillingOn properties for good measure, although this
isn't strictly necessary.

Note: you shouldn't delete the user's entire account here or any data you may have stored in
your database related to them (e.g. imported orders, etc.). Shopify's app store guidelines state
that a user should be able to reinstall your app at any point and pick up where they left off;
deleting their data means they'll start with a blank slate, violating the guidelines. Instead, Shopify
will send a separate GDPR privacy webhook when it decides a user's account data should be
deleted.

(We'll set up the GDPR privacy webhooks in this chapter too.)

Alright, let's start writing the handler for the "app/uninstalled" webhook. All webhooks from
Shopify will be HTTP POST requests, which means we decorate the handlers with the
[HttpPost] attribute, telling ASP.NET to only route requests to the handlers if they're POST

requests. Additionally, all webhooks from Shopify will have a shop querystring parameter
which can be used to determine which shop the event happened on:

Controllers/WebhooksController.cs

// ...

[ValidateShopifyWebhook]
public class WebhooksController : Controller
{
// ...

89
Chapter 10: Validating and handling Shopify's webhooks

[HttpPost]
public async Task<StatusCodeResult> AppUninstalled([FromQuery] string shop)
{
// TODO: find the user based on the shop value
}
}

We use the shop value to find whichever user this shop belongs to, but we need to make sure
that value is not null as well -- otherwise we'd look up any user who has a null
ShopifyShopDomain value in their user record. Assuming the value isn't null, we next need to

check if the user is null, just to make sure their account hasn't been deleted or something strange
has occurred to make Shopify think the app has a user that the app doesn't think it has.

This is a situation where we'd want to log a warning message, just so you have a "paper trail" to
follow if you need to debug a webhook later on. Logging the warning message will print out a
message to the terminal or wherever else you might configure the application to log, and if you
deploy on a platform like Azure you'll be able to look up those logs later.

Controllers/WebhooksController.cs

// ...

[ValidateShopifyWebhook]
public class WebhooksController : Controller
{
// ...

[HttpPost]
public async Task<StatusCodeResult> AppUninstalled([FromQuery] string shop)
{
if (string.IsNullOrWhiteSpace(shop))
{
_logger.LogWarning("Received AppUninstalled webhook but the shop value
was null or empty.");
return Ok();
}

// Pull in the user


var user = await _userContext.Users.FirstOrDefaultAsync(u =>
u.ShopifyShopDomain == shop);

if (user == null)
{
_logger.LogWarning("Received AppUninstalled webhook for shop {shop},
but a user with that shop value could not be found.", shop);
// User does not exist or may have already been deleted
return Ok();
}

90
Chapter 10: Validating and handling Shopify's webhooks

// TODO: delete the user's charge ID, access token, shop domain and
billing date
}
}

Remember, webhooks must return a 200 OK status code. If they don't they'll be retried
over and over until finally the failing webhook is deleted after 48 hours. Shopify will send
you multiple warning emails when a webhook is failing, and they'll send you one final email
when it gets deleted. At that point, you'll need to use the Shopify API to pull in things you
might have missed, and then reconfigure the webhook to get it sending again.

A failing or deleted webhook will not affect the webhooks for other stores who've installed
your app. That is to say, if the "orders/create" webhook fails for John's store and gets
deleted, it will still run for Jane's store.

At this point, the only thing left for the webhook handler to do is log a simple informational
message saying the app is handling the "app/uninstalled" webhook, and then delete the all of the
user's Shopify data except for their ShopifyShopId . We'll need that shop ID to handle the
GDPR privacy webhooks.

Don't forget: the user's access token is already invalid at this point, so it's not possible to
make any calls to the Shopify API in this webhook handler.

Controllers/WebhooksController.cs

// ...

[ValidateShopifyWebhook]
public class WebhooksController : Controller
{
// ...

[HttpPost]
public async Task<StatusCodeResult> AppUninstalled([FromQuery] string shop)
{
// ...

_logger.LogInformation("Handling 'app/uninstalled' webhook for shop


{shop}", shop);

// Delete the user's subscription and Shopify details, leaving their


ShopifyShopId intact
user.ShopifyChargeId = null;
user.ShopifyAccessToken = null;
user.ShopifyShopDomain = null;
user.BillingOn = null;

91
Chapter 10: Validating and handling Shopify's webhooks

// Save the changes and return 200 OK


await _userContext.SaveChangesAsync();

return Ok();
}
}

After the webhook handler runs, the user's database record should now be reset to a state where
the application does not think they're subscribed or connected via OAuth. The app will still let
them return, reinstall the application and restart their subscription in accordance with Shopify's
guidelines. Around two to three days after the user uninstalls the application, Shopify will send a
GDPR Shop Redacted webhook, which tells the app that it's time to fully delete the user's account
and any data connected to it.

The GDPR privacy webhooks


A relatively recent change to Shopify's app store guidelines is that all apps published on their app
store must set up and handle their mandatory GDPR webhooks. Shopify uses the GDPR
webhooks to comply with the European Union's privacy laws (General Data Protection
Regulation). Getting into the GDPR is opening up a whole can of worms, but to summarize how
Shopify has implemented it, all applications must delete any data that could be used to identify a
person once the appropriate GDPR webhook has been sent.

The person in this case is either a shop owner who has uninstalled your app, or a customer of a
shop that has installed your app. Additionally, according to the GDPR laws, those customers may
at any time send a request to Shopify asking for a copy of whatever data Shopify might have
saved on them. If that customer has purchased anything from a store that has installed your app,
you'll get a data request webhook. When that happens, you're responsible for compiling all of the
data you have on that customer and sending it to the store owner.

That might sound like a lot of hassle, but the laws are designed to discourage applications from
storing more data than they strictly need to operate. Of course, there are totally legitimate reasons
your app might need to store that kind of data, which is why it isn't against Shopify's guidelines
to do so. If your application does need that data, you just need to make sure you're compliant with
the GDPR webhooks that we're about to write.

Remember, regardless of whether you store personally identifiable data or not, you do need
to set up and handle these webhooks. It's mandatory for all Shopify applications.

92
Chapter 10: Validating and handling Shopify's webhooks

If you recall from the very first chapter of this book, we set up three different GDPR webhook
URLs when "provisioning" the app in Shopify's partner dashboard. If you didn't do that, you can
go back to the app's settings in the partner dashboard at any time and configure the webhook
URLs. There are a total of three GDPR webhooks to handle:

1. The Customer Data Request webhook, sent to "/Webhooks/GdprCustomerDataRequest".


2. The Customer Redacted webhook, sent to "/Webhooks/GdprCustomerRedacted".
3. The Shop Redacted webhook, sent to "/Webhooks/GdprShopRedacted".

To begin with, the Customer Data Request webhook is the one your app will receive when a
customer of any shop that's installed your app asks Shopify to send them a copy of all data stored
on them. Shopify will let the store owner know that this has happened, and then it will ping the
app to let you know that you must compile a copy of all data you have on the customer. Once
you've got that data compiled, you must send it to the store owner, not Shopify.

Like all webhooks, the GDPR webhooks will be sent as HTTP POST messages, which means the
action should be decorated with an [HttpPost] attribute. Inside the request body, Shopify will
send an object containing only the essential data needed to identify the customer who has made
the data request. This includes their Shopify API ID, their email address, their phone number, and
the Shopify store's API ID. It also includes a list of order IDs for orders that were purchased by
the customer.

ShopifySharp provides a class called the CustomerDataRequestWebhook which you can use to
deserialize the request message data into an object we can work with:

Controllers/WebhooksController.cs

// ...

[ValidateShopifyWebhook]
public class WebhooksController : Controller
{
// ...

[HttpPost]
public async Task<StatusCodeResult> GdprCustomerDataRequest()
{
// Deserialize the request body into an object containing the customer's
ID
var data = await
Request.DeserializeBodyAsync<ShopifySharp.CustomerDataRequestWebhook>();

// TODO: handle the data request


}
}

93
Chapter 10: Validating and handling Shopify's webhooks

In this AuntieDot base project, we're not storing any data at all related to the customers of our
users, so there's nothing that needs to be done in this webhook handler. Indeed, depending on
your own use case, it may not even be possible to gather that data in an automated fashion -- it
might take your own custom tool or SQL queries to compile it.

Regardless, the webhook handler is going to log a critical message here so we know when
Shopify sends the webhook. You'll be able to find this message in the application logs when
deployed to platforms like Azure or AWS. If you're really fancy, you might even want to set up
an email message or notification sent to yourself when this webhook is received.

That's a little bit beyond the scope of this guide, though, so we'll just log a message and make
sure we scan the logs or set up an alert in Azure/AWS when a critical message is logged:

Controllers/WebhooksController.cs

// ...

[ValidateShopifyWebhook]
public class WebhooksController : Controller
{
// ...

[HttpPost]
public async Task<StatusCodeResult> GdprCustomerDataRequest()
{
// ...

// Log the customer ID, the shop ID, and the orders that were made by the
customer
var requestedOrders = string.Join(", ", data.OrdersRequested ??
Enumerable.Empty<long>());
var message = "Customer {0} has requested their data via shop {1} ({2}).
Orders requested: {3}";

_logger.LogCritical(message, data.Customer.Id, data.ShopId,


data.ShopDomain, requestedOrders);

return Ok();
}
}

Once the request is received and logged, the message will look something like this:

94
Chapter 10: Validating and handling Shopify's webhooks

While the data request does include the customer's email address and their phone number, it
may not be wise to log those things as technically your logs would be storing their
identifiable information. Depending on you manage your log files in production, it may be
difficult to delete that information from the logs or even determine if you have it stored in
the first place.

If you do log or use their email address or phone number, keep in mind that one or the either
can be null! A customer can check out with just a phone number or just an email address, so
you'll want to do some basic null checking on both of those values before using them.

The next GDPR webhook is the Customer Redacted webhook. This one gets sent when a
customer asks Shopify to delete all data they have pertaining to the customer. If they had
purchased something from a store that has your app installed, you'll receive this webhook
instructing you to delete their data.

For this webhook, the request body is almost exactly the same as the last one, but we deserialize
it into a ShopifySharp.CustomerRedactedWebhook instead. You'll find the same properties like
the customer's API ID, email address and phone number, along with the shop's ID and domain.

Once again, the AuntieDot app isn't storing any customer data, so it doesn't need to do anything
here but log a message and return 200 OK:

Controllers/WebhooksController.cs

// ...

[ValidateShopifyWebhook]
public class WebhooksController : Controller
{
// ...

[HttpPost]
public async Task<StatusCodeResult> GdprCustomerRedacted()
{
var data = await
Request.DeserializeBodyAsync<ShopifySharp.CustomerRedactedWebhook>();
var message = "Customer {0} has been redacted via shop {1} ({2})";

_logger.LogWarning(message, data.Customer.Id, data.ShopId,


data.ShopDomain);

// This app does not currently log customer data, nothing to do here

return Ok();
}
}

95
Chapter 10: Validating and handling Shopify's webhooks

Even though the information is available in the data variable, this is another place where
you shouldn't log the customer's email address or phone number. That's exactly the kind of
personally identifiable information that the webhook is telling you to delete!

Finally, the last webhook we're going to implement (for this base AuntieDot project) is the GDPR
Shop Redacted webhook. Conceptually, this one's kind of like an extension of the
"app/uninstalled" webhook; that one gets sent as soon as a user uninstalls your app, and you're
expected to delete their access token but leave their account intact so they can reinstall and pick
back up where they left off. With this GDPR webhook, though, you're expected to delete
everything pertaining to the user/shop so it looks like they had never installed or used the app at
all.

The timing isn't exact, but Shopify will usually send the GDPR Shop Redacted webhook about 48
hours after it sends the "app/uninstalled" webhook. If the shop owner reinstalls your app at any
point before those 48 hours are up, the GDPR webhook will be postponed and canceled.

To handle the webhook, the first thing you'll want to do is deserialize the request body into a
ShopifySharp.ShopRedactedWebhook object. This object doesn't have as much data as the last

two webhook handlers got, but it does contain the shop's API ID and Shopify domain (e.g.
"example.myshopify.com").

As always, we should log a warning message that this webhook has been received. In this case it's
okay to log the shop domain here, as that's not personally identifiable information (i.e. it's not an
email address, phone number or name).

Controllers/WebhooksController.cs

// ...

[ValidateShopifyWebhook]
public class WebhooksController : Controller
{
// ...

[HttpPost]
public async Task<StatusCodeResult> GdprShopRedacted()
{
var data = await
Request.DeserializeBodyAsync<ShopifySharp.ShopRedactedWebhook>();
var message = "Shop {0} ({1}) has been redacted";

_logger.LogWarning(message, data.ShopId, data.ShopDomain);

// TODO: pull in the user and delete their data


}
}

96
Chapter 10: Validating and handling Shopify's webhooks

With the message logged, we now need to pull in the user's record from the database and delete it
permanently. Don't try to be tricky and add a Delete = true column to your SQL database
table -- that would be cheating! The user's data needs to be completely removed and
unrecoverable if we're to follow Shopify's app store guidelines.

Since the app store's the user's shop ID and Shopify domain in the database, the action can easily
use the ShopId and ShopDomain properties on the ShopifySharp.ShopRedactedWebhook to
find the user's database record. You'll need to check if the user record exists though (i.e. check
that it's not null after you pull it in). We do that because Shopify will send this webhook multiple
times if the app doesn't respond quickly enough, where "quickly enough" is an obscure term that
Shopify doesn't define.

If the user is null, it probably means the webhook was already received and handled. Assuming
the user isn't null, you'll need to delete any OAuthState records belonging to them, and then
delete the user record itself.

Controllers/WebhooksController.cs

// ...

[ValidateShopifyWebhook]
public class WebhooksController : Controller
{
// ...

[HttpPost]
public async Task<StatusCodeResult> GdprShopRedacted()
{
// ...

// Pull in the user and delete their data


var user = await _userContext.Users.FirstOrDefaultAsync(u =>
u.ShopifyShopId == data.ShopId);

if (user != null)
{
// Delete the user's oauth states and their user record
var oauthStates = await _userContext.States
.Where(state => state.ShopifyShopDomain == user.ShopifyShopDomain)
.ToListAsync();

_userContext.States.RemoveRange(oauthStates);
_userContext.Users.Remove(user);

await _userContext.SaveChangesAsync();
}

97
Chapter 10: Validating and handling Shopify's webhooks

return Ok();
}
}

Done! The user and any OAuth state tokens connected to their account have been deleted. At this
point, as far as the application is concerned, it will think it has never seen this user before and
they've never installed the app. If they ever come back to reinstall the app, they'll start off fresh
with a new user account.

And now with the GDPR Shop Redacted webhook handler implemented, the base AuntieDot
project is in full compliance with Shopify's app store guidelines and could be published to the app
store! In the next chapter, we'll take it for a test drive to make sure everything's working the way
it should work, and then we can move on to more complex application features like proxy pages,
embedded apps, and custom JavaScript scripts running on the users' storefronts.

98
Chapter 11: Taking AuntieDot for a test drive

Taking AuntieDot for a test drive


This is where everything comes together — it's time to test the AuntieDot app out to make sure it
all works the way we'd expect. To do that, you'll need to have a Shopify developer store set up.
Here's how you can quickly set one up if you haven't done so already:

1. Log in to your Shopify developer dashboard at


https://app.shopify.com/services/partners/auth/login.
2. Select "Development Stores" on the left navigation menu, and then "Create a new
development store" at the top right of the page.
3. Set the "What kind of store would you like?" option to "Online store", and then fill out the
rest of the form to create the shop.
4. Navigate back to "Development Stores" on the left navigation menu, then find your new
store's URL and copy it down.

With your development store ready to go, let's start testing out the entire process. In your
terminal, make sure you're in your project's directory ( cd path/to/my/project ), and then type
dotnet run to start the application. You'll see several lines that look something like this:

Terminal output

info: Microsoft.Hosting.Lifetime[0]
Now listening on: https://localhost:5001
info: Microsoft.Hosting.Lifetime[0]
Now listening on: http://localhost:5000
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
# and so on

If instead you see an error message saying dotnet couldn't find a project to run, your terminal is
probably not in the right directory. It's possible that you're actually in the project solution
directory, and your actual project is in one of the subfolders. This is common in dotnet projects,
so look for a subfolder with the same name as your project (e.g. "AuntieDot") and change to that,
then run dotnet run again.

Once you see the messages indicating the application has started, you'll want to take note of
which addresses it says the application is running on. The non-http address is what we'll be using
with our localhost forwarder tool. It looks like (and probably is) http://localhost:5000 .
Remember this address, you'll need it in a few moments for testing webhooks.

99
Chapter 11: Taking AuntieDot for a test drive

Type that address into your web browser, and you should be taken straight to the app's login
page:

Here you should enter your test Shopify store's domain -- e.g. my-test-shop.myshopify.com --
and then click the button. If you receive an error message about the SQL database connection
after submitting the form, your SQL database is either not running, or your app cannot connect to
it. Assuming you set up a SQL database using the Docker example from Chapter 1, you can start
the database by using docker start shopify-sql-database .

As long as the database is running when you submit that form, you should be sent to Shopify
where you'll be asked if you want to install the app and add it to your development store:

100
Chapter 11: Taking AuntieDot for a test drive

After confirming the installation, you should next see the form we wrote asking users to
subscribe to a monthly recurring subscription plan:

And when you press that button, you'll be taken back to Shopify where they give you the details
about the subscription charge. If you're running the app locally on your machine, this page should
clearly state that the charge is in test mode and you will not actually be charged when you start
it:

101
Chapter 11: Taking AuntieDot for a test drive

If you accept the charge, you'll be sent back to the app for the final time where you can finally
start using it! At this point you'll see a list of all the open orders on your Shopify store.

It should be noted that if you're using a development store and have not placed any orders,
the list here is going to be completely empty.

102
Chapter 11: Taking AuntieDot for a test drive

And finally, if you click the "Subscription" link at the top of the page, the app should load a page
with a breakdown of your current subscription. This includes whether it's a test charge (it should
be, if you're running on your personal machine), the name and price of the plan, and when your
free trial ends.

Testing the "app/uninstalled" webhook.


The last thing we need to test is the "app/uninstalled" webhook. However, because webhooks
cannot be sent to localhost (an address only reachable on your local network), you'll need to have
the app accessible via the web. You can use your favorite localhost forwarder to do this, or you'll
need to deploy your app to a server.

Ngrok was the forwarding service recommended at the start of this book, so if you're using that
you can start it up with this command (you want to use the port that you saw in the localhost
address, e.g. localhost:5000 ; replace port 5000 with a different one if you've customized the
localhost port at some point):

ngrok http 5000 --subdomain MySubdomainHere

Note: if you're not using a paid ngrok plan, your subdomain will be randomized which
means you need to recreate the webhooks to use that subdomain.

103
Chapter 11: Taking AuntieDot for a test drive

When you've got your localhost forwarder running and the app is reachable outside of your own
network, you can test the "app/uninstalled" webhook by finding it in the list of installed apps on
your dev store's admin dashboard. Click the "Delete" link, and Shopify will ask you to give
feedback about why you're uninstalling it; you can just write "testing the app" in the dialog.

Wait a few seconds for the webhook to send (you'll probably see some activity in your terminal),
then head over to your app and try to navigate to the /Home page. If everything works correctly,
you'll be redirected to the login page; that's because, even though your auth cookie says you're
subscribed and logged in, the HomeController.Index action pulls in your user record from the
database and checks that your access token isn't null.

So, even though the "app/uninstalled" webhook is "asynchronous" in the sense that it can be sent
when you're not even on the website (and the app has no way to update your cookie when it
handles the webhook), the app is still checking that it still has API access where it matters.

If you don't get redirected after waiting a couple of minutes, it's likely that your webhook
handler either failed; you'll see an error in the terminal if this is the case. If you see no error,
the next mostly likely issue is that your app may not reachable, in which case you should
check your localhost forwarder and make sure the domain it's using is the same one the app
uses when creating webhooks; it should match the ISecrets.HostDomain value.

104
Chapter 11: Taking AuntieDot for a test drive

As far as testing the GDPR webhooks goes, as far as we've been able to tell, there's no way to test
those easily. Shopify doesn't seem to send the GDPR webhooks for development stores. The next
best thing you could do is either trust that your app will handle them correctly (and monitor your
logs for issues), or disable the validation attribute during development so you can send your own
fake webhook data at the endpoint to see what happens.

But we can at least end this chapter with a little bit of good news! Remember that uninstall
feedback dialog? You can actually view the feedback that merchants leave when they uninstall
your app! Just go to your partner dashboard and click your app, then go to the "Latest app
history" section toward the bottom. Next to the uninstall events, you'll see the feedback the
merchant has left in this uninstall dialog.

105
Chapter 11: Taking AuntieDot for a test drive

106

You might also like