You are on page 1of 63

WEB DEVELOPMENT

Building a Note-Taking Softwareas-a-Service Using ASP.NET MVC


5, Stripe, and Azure
by Pedro Alonso5 days ago1 Comment

75

107

Share

What You'll Be Creating

1. Introduction
In this tutorial, I'm going to show you how to build a Software-as-a-Service (SaaS) minimum viable product
(MVP). To keep things simple, the software is going to allow our customers to save a list of notes.
I am going to offer three subscription plans: the Basic plan will have a limit of 100 notes per user, the
Professional plan will allow customers to save up to 10,000 notes, and the Business plan will allow a
million notes. The plans are going to cost $10, $20 and $30 per month respectively. In order to receive payment
from our customers, I'm going to use Stripe as a payment gateway, and the website is going to be deployed to
Azure.

2. Setup
2.1 Stripe
In a very short time Stripe has become a very well known Payment Gateway, mainly because of their developerfriendly approach, with simple and well-documented APIs. Their pricing is also very clear: 2.9% per transaction
+ 30 cents. No setup fees or hidden charges.

Credit card data is also very sensitive data, and in order to be allowed to receive and store that data in my
server, I need to be PCI compliant. Because that's not an easy or quick task for most small companies, the
approach that many payment gateways take is: You display the order details, and when the customer agrees to
purchase, you redirect the customer to a page hosted by the payment gateway (bank, PayPal, etc), and then
they redirect the customer back.
Stripe has a nicer approach to this problem. They offer a JavaScript API, so we can send the credit card number
directly from the front-end to Stripe's servers. They return a one-time use token that we can save to our
database. Now, we only need an SSL certificate for our website that we can quickly purchase from about $5 per
year.
Now, sign up for a Stripe account, as you'll need it to charge your customers.

2.2 Azure
As a developer I don't want to be dealing with dev-ops tasks and managing servers if I don't have to. Azure
websites is my choice for hosting, because it's a fully managed Platform-as-a-Service. It allows me to deploy
from Visual Studio or Git, I can scale it easily if my service is successful, and I can focus on improving my
application. They offer $200 to spend on all Azure services in the first month to new customers. That's enough
to pay for the services that I am using for this MVP. Sign up for Azure.

2.3 Mandrill and Mailchimp: Transactional Email


Sending emails from our application might not seem like a very complex task, but I would like to monitor how
many emails are delivered successfully, and also design responsive templates easily. This is what Mandrill
offers, and they also let us send up to 12,000 emails per month for free. Mandrill is built by MailChimp, so they
know about the business of sending emails. Also, we can create our templates from MailChimp, export them to
Mandrill, and send emails from our app using our templates. Sign up for Mandrill, and sign up for MailChimp.

2.4 Visual Studio 2013 Community Edition


Last but not least, we need Visual Studio to write our application. This edition, which was launched only a few
months ago, is completely free and is pretty much equivalent to Visual Studio Professional. You can download it
here, and this is all we need, so now we can focus on the development.

3. Creating the Website

The first thing that we need to do is open Visual Studio 2013. Create a new ASP.NET Web Application:

Go to File > New Project and choose ASP.NET Web Application.

On the ASP.NET template dialog, choose the MVC template and selectIndividual User Accounts.

This project creates an application where a user can login by registering an account with the website. The
website is styled using Bootstrap, and I'll continue building the rest of the app with Bootstrap. If you hit F5 in
Visual Studio to run the application, this is what you will see:

This is the default landing page, and this page is one of the most important steps to convert our visitors into
customers. We need to explain the product, show the price for each plan, and offer them the chance to sign up
for a free trial. For this application I am creating three different subscription plans:

Basic: $10 per month

Professional: $20 per month

Business: $30 per month

3.1 Landing Page


For some help creating a landing page, you can visit ThemeForest and purchase a template. For this sample, I
am using a free template, and you can see the final result in the photo below.

3.2 Registration Page


In the website that we created in the previous step, we also get a Registration form template. From the landing
page, when you navigate to Prices, and click on Free Trial, you navigate to the registration page. This is the
default design:

We only need one extra field here to identify the subscription plan that the user is joining. If you can see in the
navigation bar of the photo, I am passing that as a GET parameter. In order to do that, I generate the markup for
the links in the landing page using this line of code:
1 <a href="@Url.Action("Register", "Account", new { plan = "business" })">
Free Trial
2
</a>
3
To bind the Subscription Plan to the back-end, I need to modify the class RegisterViewModel and add the new
property.
01
02
03
04
05
06
07
08
09
10

public class RegisterViewModel


{
[Required]
[EmailAddress]
[Display(Name = "Email")]
public string Email { get; set; }

[Required]
[StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumL
6)]

[DataType(DataType.Password)]
[Display(Name = "Password")]
public string Password { get; set; }

11
12
13
14
15
16
17
18
19
20

[DataType(DataType.Password)]
[Display(Name = "Confirm password")]
[Compare("Password", ErrorMessage = "The password and confirmation password do not match.")
public string ConfirmPassword { get; set; }
public string SubscriptionPlan { get; set; }
}

I also have to edit AccountController.cs, and modify the Action Register to receive the plan:
1
2
3
4
5
6
7
8

[AllowAnonymous]
public ActionResult Register(string plan)
{
return View(new RegisterViewModel
{
SubscriptionPlan = plan
});
}

Now, I have to render the Plan Identifier in a hidden field in the Register form:
1

@Html.HiddenFor(m => m.SubscriptionPlan)

The last step will be to subscribe the user to the plan, but we'll get to that a bit later. I also update the design of
the registration form.

3.3 Login Page

In the template we also get a login page and action controllers implemented. The only thing I need to do is
to make it look prettier.

3.4 Forgot Password


Take a second look at the previous screenshot, and you'll notice that I added a "Forgot your Password?" link.
This is already implemented in the template, but it's commented out by default. I don't like the default behaviour,
where the user needs to have the email address confirmed to be able to reset the password. Let's remove that
restriction. In the file AccountController.cs edit the action ForgotPassword :
01
02
03
04
05
06
07
08

[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> ForgotPassword(ForgotPasswordViewModel model)
{
if (ModelState.IsValid)
{
var user = await UserManager.FindByNameAsync(model.Email);
if (user == null)
{

09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

// Don't reveal that the user does not exist or is not confirmed
return View("ForgotPasswordConfirmation");

// For more information on how to enable account confirmation and password reset please
// Send an email with this link
// string code = await UserManager.GeneratePasswordResetTokenAsync(user.Id);
// var callbackUrl = Url.Action("ResetPassword", "Account", new { userId = user.Id, cod
// await UserManager.SendEmailAsync(user.Id, "Reset Password", "Please reset your passw
"\">here</a>");
// return RedirectToAction("ForgotPasswordConfirmation", "Account");
}
// If we got this far, something failed, redisplay form
return View(model);
}

The code to send the email with the link to reset the password is commented out. I'll show how to implement
that part a bit later. The only thing left for now is to update the design of the pages:

ForgotPassword.cshtml: Form that is displayed to the user to enter his or her email.

ForgotPasswordConfirmation.cshtml: Confirmation message after the reset link has been emailed to the
user.

ResetPassword.cshtml: Form to reset the password after navigating to the reset link from the email.

ResetPasswordConfirmation.cshtml: Confirmation message after the password has been reset.

4. ASP.NET Identity 2.0


ASP.NET Identity is a fairly new library that has been built based on the assumption that users will no longer log
in by using only a username and password. OAuth integration to allow users to log in through social channels
such as Facebook, Twitter, and others is very easy now. Also, this library can be used with Web API, and
SignalR.
On the other hand, the persistence layer can be replaced, and it's easy to plug in different storage mechanisms
such as NoSQL databases. For the purposes of this application, I will use Entity Framework and SQL Server.
The project that we just created contains the following three NuGet packages for ASP.NET Identity:

Microsoft.AspNet.Identity.Core: This package contains the core interfaces for ASP.NET Identity.

Microsoft.AspNet.Identity.EntityFramework: This package has the Entity Framework implementation


of the previous library. It will persist the data to SQL Server.

Microsoft.AspNet.Identity.Owin: This package plugs the middle-ware OWIN authentication with


ASP.NET Identity.

The main configuration for Identity is in App_Start/IdentityConfig.cs. This is the code that initializes Identity.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

public static ApplicationUserManager Create(IdentityFactoryOptions<ApplicationUserManager> optio


{
var manager = new ApplicationUserManager(new UserStore<ApplicationUser>(context.Get<Applicat
// Configure validation logic for usernames
manager.UserValidator = new UserValidator<ApplicationUser>(manager)
{
AllowOnlyAlphanumericUserNames = false,
RequireUniqueEmail = true
};
// Configure validation logic for passwords
manager.PasswordValidator = new PasswordValidator
{
RequiredLength = 6,
RequireNonLetterOrDigit = true,
RequireDigit = true,
RequireLowercase = true,
RequireUppercase = true,
};
// Configure user lockout defaults
manager.UserLockoutEnabledByDefault = true;
manager.DefaultAccountLockoutTimeSpan = TimeSpan.FromMinutes(5);
manager.MaxFailedAccessAttemptsBeforeLockout = 5;
// Register two factor authentication providers. This application uses Phone and Emails as

user

// You can write your own provider and plug it in here.


manager.RegisterTwoFactorProvider("Phone Code", new PhoneNumberTokenProvider<ApplicationUse
{
MessageFormat = "Your security code is {0}"
});
manager.RegisterTwoFactorProvider("Email Code", new EmailTokenProvider<ApplicationUser>
{
Subject = "Security Code",
BodyFormat = "Your security code is {0}"
});
manager.EmailService = new EmailService();
manager.SmsService = new SmsService();
var dataProtectionProvider = options.DataProtectionProvider;
if (dataProtectionProvider != null)
{
manager.UserTokenProvider =
new DataProtectorTokenProvider<ApplicationUser>(dataProtectionProvider.Create("ASP.
}
return manager;

42
43
44
45
46
As you can see in the code, it's pretty easy to configure users' validators and password validators, and two
factor authentication can also be enabled. For this application, I use cookie-based authentication. The cookie is
generated by the framework and is encrypted. This way, we can scale horizontally, adding more servers if our
application needs it.

5. Sending Emails With Mandrill


You can use MailChimp to design email templates, and Mandrill to send emails from your application. In the first
place you need to link your Mandrill account to your MailChimp account:

Log in to MailChimp, click your username in the right-hand panel, and selectAccount from the dropdown.

Click on Integrations and find the Mandrill option in the list of integrations.

Click on it to see the integration details, and click the Authorize Connectionbutton. You will be
redirected to Mandrill. Allow the connection, and the integration will be completed.

5.1 Creating the "Welcome to My Notes" Email Template


Navigate to Templates in MailChimp, and click on Create Template.

Now, select one of the templates offered by MailChimp. I selected the first one:

In the template editor, we modify the content as we like. One thing to note, as you can see below, is that we can
use variables. The format is *|VARIABLE_NAME|* . From the code, we'll set those for each customer. When you
are ready, click on Save and Exit at the bottom right.

In the Templates list, click on Edit, on the right side, and select Send To Mandrill. After a few seconds you will
get a confirmation message.

To confirm that the template has been exported, navigate to Mandrill and log in. Select Outbound from the left
menu, and then Templates from the top menu. In the image below you can see that the template has been
exported.

If you click on the name of the template, you'll see more information about the template. The field "Template
Slug" is the text identifier that we will use in our application to let Mandrill API know which template we want to
use for the email that we are sending.

I leave it as an exercise for you to create a "Reset Password" template.

5.2 Sending Emails From My Notes

In the first place, install Mandrill from NuGet. After that, add your Mandrill API Key to Web.config App
Settings. Now, open App_Start/IdentityConfig.cs and you'll see the class EmailService skeleton pending
implementation:

1
public class EmailService : IIdentityMessageService
2 {
3
public Task SendAsync(IdentityMessage message)
{
4
// Plug in your email service here to send an email.
5
return Task.FromResult(0);
6
}
7 }
8
Although this class has only the method SendAsync , because we have two different templates (Welcome Email
Template and Reset Password Template), we will implement new methods. The final implementation will look
like this.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

public class EmailService : IIdentityMessageService


{
private readonly MandrillApi _mandrill;
private const string EmailFromAddress = "no-reply@mynotes.com";
private const string EmailFromName = "My Notes";
public EmailService()
{
_mandrill = new MandrillApi(ConfigurationManager.AppSettings["MandrillApiKey"]);
}
public Task SendAsync(IdentityMessage message)
{
var task = _mandrill.SendMessageAsync(new EmailMessage
{
from_email = EmailFromAddress,
from_name = EmailFromName,
subject = message.Subject,
to = new List<Mandrill.EmailAddress> { new EmailAddress(message.Destination) },
html = message.Body
});
return task;
}
public Task SendWelcomeEmail(string firstName, string email)
{
const string subject = "Welcome to My Notes";
var emailMessage = new EmailMessage
{
from_email = EmailFromAddress,
from_name = EmailFromName,
subject = subject,
to = new List<Mandrill.EmailAddress> { new EmailAddress(email) },
merge = true,
};
emailMessage.AddGlobalVariable("subject", subject);
emailMessage.AddGlobalVariable("first_name", firstName);
var task = _mandrill.SendMessageAsync(emailMessage, "welcome-my-notes-saas", null);
task.Wait();

38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68

return task;
}
public Task SendResetPasswordEmail(string firstName, string email, string resetLink)
{
const string subject = "Reset My Notes Password Request";
var emailMessage = new EmailMessage
{
from_email = EmailFromAddress,
from_name = EmailFromName,
subject = subject,
to = new List<Mandrill.EmailAddress> { new EmailAddress(email) }
};
emailMessage.AddGlobalVariable("subject", subject);
emailMessage.AddGlobalVariable("FIRST_NAME", firstName);
emailMessage.AddGlobalVariable("RESET_PASSWORD_LINK", resetLink);

var task = _mandrill.SendMessageAsync(emailMessage, "reset-password-my-notes-saas", nul


return task;
}

To send an email through Mandrill API:


1. Create email message.
2. Set message variables' values.
3. Send email specifying the template slug.
In AccountController -> Register action, this is the code snippet to send the welcome email:
1

await _userManager.EmailService.SendWelcomeEmail(user.UserName, user.Email);

In AccountController -> ForgotPassword action, this is the code to send the email:
1
2
3
4

// Send an email to reset password


string code = await UserManager.GeneratePasswordResetTokenAsync(user.Id);
var callbackUrl = Url.Action("ResetPassword", "Account", new { userId = user.Id, code = code }, p
Request.Url.Scheme);
await UserManager.EmailService.SendResetPasswordEmail(user.UserName, user.Email, callbackUrl);

6. Integrating SAAS Ecom for Billing


One important thing in SAAS applications is billing. We need to have a way to charge our
customers periodically, monthly in this example. Because this part is something that requires a lot of work, but
doesn't add anything valuable to the product that we are selling, we are going to use the open source library
SAAS Ecom that was created for this purpose.

6.1 Data Model: Entity Framework Code First


SAAS Ecom has a dependency on Entity Framework Code First. For those of you that are not familiar with it,
Entity Framework Code First allows you to focus on creating C# POCO classes, letting Entity Framework map
the classes to database tables. It follows the idea of convention over configuration, but you can still specify
mappings, foreign keys and so on, if needed.
To add SAAS Ecom to our project, just install the dependency using NuGet. The library is split in two packages:
SaasEcom.Core that contains the business logic, and SaasEcom.FrontEnd that contains some view helpers to
use in an MVC application. Go ahead and install SaasEcom.FrontEnd.

You can see that some files have been added to your solution:

Content/card-icons: Credit card icons to display in the billing area

Controllers/BillingController: Main controller

Controllers/StripeWebhooksController: Stripe Webhooks

Scripts/saasecom.card.form.js: Script to add credit card to Stripe

Views/Billing: Views and view partials

There are still a few steps left to integrate SAAS Ecom, so get your Stripe API Keys and add them to
Web.config.
1
2

<appSettings>
<add key="StripeApiSecretKey" value="your_key_here" />
<add key="StripeApiPublishableKey" value="your_key_here" />

3
4

</appSettings>

If you try to compile, you'll see errors:


Open the file Models/IdentityModels.cs, and then make the class ApplicationUser inherit from SaasEcomUser.
1

ApplicationUser : SaasEcomUser { /* your class methods*/ }

Open the file Models/IdentityModels.cs, and then your class ApplicationDbContext should inherit
from SaasEcomDbContext<ApplicationUser>.
1 ApplicationDbContext : SaasEcomDbContext<ApplicationUser>
2 { /* Your Db context properties */ }
Because ApplicationUser is inheriting from SaasEcomUser , the default behaviour for Entity Framework
would be to create two tables in the database. Because we don't need that in this case, we need to add this
method to the class ApplicationDbContext to specify that it should use only one table:
1 protected override void OnModelCreating(DbModelBuilder modelBuilder)
2 {
modelBuilder.Entity<ApplicationUser>().Map(m => m.MapInheritedProperties());
3
base.OnModelCreating(modelBuilder);
4
}
5
As we just updated the DbContext , to make it inherit from SaasEcomDbContext , the database has to be
updated too. In order to do that, enable code migrations and update the database opening NuGet Package
Manager from the menu Tools > NuGet Package Manager > Package Manager Console:
1 PM > enable-migrations
2 PM > add-migration Initial
3 PM > update-database
If you get an error when you run update-database , the database (SQL Compact) is inside your AppData
folder, so open the database, delete all the tables in it, and then run update-database again.

6.2 Creating the Subscription Plans in Stripe and Database


The next step in the project is to integrate Stripe to charge our customers monthly, and for that we need to
create the subscription plans and pricing in Stripe. So sign in to your Stripe dashboard, and create your
subscription plans as you can see in the pictures.

Once we have created the Subscription Plans in Stripe, let's add them to the database. We do this so that we
don't have to query Stripe API each time that we need any information related to subscription plans.
Also, we can store specific properties related to each plan. In this example, I'm saving as a property of each
plan the number of notes that a user can save: 100 notes for the basic plan, 10,000 for the professional, and 1
million for the business plan. We add that information to the Seed method that is executed each time that the
database is updated when we run update-database from NuGet Package Manager console.
Open the file Migrations/Configuration.cs and add this method:
01
02
03
04
05
06
07
08
09
10

protected override void Seed(MyNotes.Models.ApplicationDbContext context)


{
// This method will be called after migrating to the latest version.
var basicMonthly = new SubscriptionPlan
{
Id = "basic_monthly",
Name = "Basic",
Interval = SubscriptionPlan.SubscriptionInterval.Monthly,
TrialPeriodInDays = 30,
Price = 10.00,
Currency = "USD"

11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

};
basicMonthly.Properties.Add(new SubscriptionPlanProperty { Key = "MaxNotes", Value = "100" }
var professionalMonthly = new SubscriptionPlan
{
Id = "professional_monthly",
Name = "Professional",
Interval = SubscriptionPlan.SubscriptionInterval.Monthly,
TrialPeriodInDays = 30,
Price = 20.00,
Currency = "USD"
};
professionalMonthly.Properties.Add(new SubscriptionPlanProperty
{
Key = "MaxNotes",
Value = "10000"
});
var businessMonthly = new SubscriptionPlan
{
Id = "business_monthly",
Name = "Business",
Interval = SubscriptionPlan.SubscriptionInterval.Monthly,
TrialPeriodInDays = 30,
Price = 30.00,
Currency = "USD"
};
businessMonthly.Properties.Add(new SubscriptionPlanProperty
{
Key = "MaxNotes",
Value = "1000000"
});

context.SubscriptionPlans.AddOrUpdate(
sp => sp.Id,
basicMonthly,
professionalMonthly,
businessMonthly);

6.3 Subscribe a Customer to a Plan on Sign-Up


The next thing that we need to do is to ensure that each time a user registers for our app, we also create the
user in Stripe using their API. To do that we use SAAS Ecom API, and we just need to edit the action Register
on AccountController and add these lines after creating the user in the database:
1

// Create Stripe user


await SubscriptionsFacade.SubscribeUserAsync(user, model.SubscriptionPlan);

2 await UserManager.UpdateAsync(user);
3
The method SubscribeUserAsync subscribes the user to the plan in Stripe, and if the user doesn't exist
already in Stripe it is created too. This is useful if you have a freemium SAAS and you only create users in
Stripe once they are on a paid plan. Another small change in the Register action
from AccountController is to save the RegistrationDate and LastLoginTime when you create the user:
1
2
3
4
5
6
7
8

var user = new ApplicationUser


{
UserName = model.Email,
Email = model.Email,
RegistrationDate = DateTime.UtcNow,
LastLoginTime = DateTime.UtcNow
};
var result = await UserManager.CreateAsync(user, model.Password);

As we need the dependency SubscriptionsFacade from SAAS Ecom, add it as a property to Account Controller:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17

private SubscriptionsFacade _subscriptionsFacade;


private SubscriptionsFacade SubscriptionsFacade
{
get
{
return _subscriptionsFacade ?? (_subscriptionsFacade = new SubscriptionsFacade(
new SubscriptionDataService<ApplicationDbContext, ApplicationUser>
(HttpContext.GetOwinContext().Get<ApplicationDbContext>()),
new SubscriptionProvider(ConfigurationManager.AppSettings["StripeApiSecretKey"]),
new CardProvider(ConfigurationManager.AppSettings["StripeApiSecretKey"],
new CardDataService<ApplicationDbContext, ApplicationUser>(Request.GetOwinConte
new CardDataService<ApplicationDbContext, ApplicationUser>(Request.GetOwinContext()
new CustomerProvider(ConfigurationManager.AppSettings["StripeApiSecretKey"]),
new SubscriptionPlanDataService<ApplicationDbContext,
ApplicationUser>(Request.GetOwinContext().Get<ApplicationDbContext>()),
new ChargeProvider(ConfigurationManager.AppSettings["StripeApiSecretKey"])));
}
}

You can simplify the way that this is instantiated using dependency injection, but this is something that can be
covered in another article.

6.4 Integrate Billing Views


When we added SAAS Ecom to the project, some view partials were added too. They use the main
_Layout.cshtml, but that layout is the one being used by the landing page. We need to add a different layout for
the web application area or customer dashboard.
I have created a very similar version to the _Layout.cshtml that is created when you add a new MVC project in
Visual Studioyou can see the_DashboardLayout.cshtml in GitHub.

The main differences are that I have added font-awesome and an area to display Bootstrap notifications if
they're present:
1
2
3
4
5
6

<div id="bootstrap_alerts">
@if (TempData.ContainsKey("flash"))
{
@Html.Partial("_Alert", TempData["flash"]);
}
</div>

For the views in the folder Views/Billing, set the layout to _DashboardLayout, otherwise it would use the default
one that is _Layout.cshtml. Do the same thing for views on the folder Views/Manage:
1

Layout = "~/Views/Shared/_DashboardLayout.cshtml";

I have slightly modified "DashboardLayout" to use some styles from the main website, and it looks like this after
signing up and navigating to the Billing section:

In the billing area a customer can Cancel or Upgrade / Downgrade a subscription. Add payment details,
using Stripe JavaScript API, so we don't need to be PCI compliant and only need SSL in the server to take
payments from our customers.

To properly test your new application, you can use several credit card numbers provided by Stripe.

The last thing that you might want to do is set up Stripe Webhooks. This is used to let Stripe notify you about
events that happen in your billing, like payment successful, payment overdue, trial about to expire, and so on
you can get a full list from the Stripe documentation. The Stripe event is sent as JSON to a public facing URL.
To test this locally you probably want to use Ngrok.
When SAAS Ecom was installed, a new controller was added to handle the webhooks from
Stripe: StripeWebhooksController.cs. You can see there how the invoice created event is handled:
01
02
03
04
05
06
07
08
09

case "invoice.payment_succeeded": // Occurs whenever an invoice attempts to be paid, and the pa


StripeInvoice stripeInvoice =
Stripe.Mapper<StripeInvoice>.MapFromJson(stripeEvent.Data.Object.ToString());
Invoice invoice = SaasEcom.Core.Infrastructure.Mappers.Map(stripeInvoice);
if (invoice != null && invoice.Total > 0)
{
// TODO get the customer billing address, we still have to instantiate the address on t
invoice.BillingAddress = new BillingAddress();
await InvoiceDataService.CreateOrUpdateAsync(invoice);

10
11
12
13
14

// TODO: Send invoice by email


}
break;

You can implement as many events in the controller as you need.

7. Building Note-Taking Functionality in Our App


The most important part of this SAAS application is to allow our customers to save notes. In order to create this
functionality, let's start by creating the Note class:
01
02 public class Note
{
03
public int Id { get; set; }
04
05
[Required]
[MaxLength(250)]
06
public string Title { get; set; }
07
08
[Required]
09
public string Text { get; set; }
10
public DateTime CreatedAt { get; set; }
11 }
12
Add a One to Many relationship from ApplicationUser to Note :
1

public virtual ICollection<Note> Notes { get; set; }

Because the DbContext has changed, we need to add a new database Migration, so open Nuget Package
Manager console and run:
1

PM> add-migration NotesAddedToModel

This is the generated code:


01
02
03
04
05
06
07
08
09
10
11
12
13

public partial class NotesAddedToModel : DbMigration


{
public override void Up()
{
CreateTable(
"dbo.Notes",
c => new
{
Id = c.Int(nullable: false, identity: true),
Title = c.String(nullable: false, maxLength: 250),
Text = c.String(nullable: false),
CreatedAt = c.DateTime(nullable: false),
ApplicationUser_Id = c.String(maxLength: 128),
})
.PrimaryKey(t => t.Id)
.ForeignKey("dbo.AspNetUsers", t => t.ApplicationUser_Id)

14
15
16
17
18
19
20
21
22
23
24
25
26
27

.Index(t => t.ApplicationUser_Id);


}
public override void Down()
{
DropForeignKey("dbo.Notes", "ApplicationUser_Id", "dbo.AspNetUsers");
DropIndex("dbo.Notes", new[] { "ApplicationUser_Id" });
DropTable("dbo.Notes");
}
}

The next thing we need is the Controller MyNotes. As we already have the model class Notes, we use the
scaffold to create the controller class to have create, read, update and delete methods using Entity Framework.
We also use the scaffold to generate the views.

At this point, after a user is registered successfully on My Notes, redirect the user to the Index action
of NotesController :
1
2

TempData["flash"] = new FlashSuccessViewModel("Congratulations! Your account has been created.")


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

So far, we have created a CRUD (Create / Read / Update / Delete) interface for Notes. We still need to check
when users try to add notes, to make sure that they have enough space in their subscriptions.
Empty list of notes:

Create new note:

List of notes:

Note detail:

Edit note:

Confirm note deletion:

I'm going to edit slightly the default markup:

In the form to create a note, I removed the CreatedAt field, and set the value in the controller.

In the form to edit a note, I changed CreatedAt to be a hidden field so that it's not editable.

I have slightly modified the CSS to make this form look a bit nicer too.

When we generated the Notes controller using Entity Framework, the list of notes was listing all the notes in the
database, not only the notes for the logged-in user. For security we need to check that users can only see,
modify or delete the notes that belong to them.
We also need to check how many notes a user has before allowing him or her to create a new one, to check
that the subscription plan limits are met. Here is the new code for NotesController:
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041

public class NotesController : Controller


{
private readonly ApplicationDbContext _db = new ApplicationDbContext();

private SubscriptionsFacade _subscriptionsFacade;


private SubscriptionsFacade SubscriptionsFacade
{
get
{
return _subscriptionsFacade ?? (_subscriptionsFacade = new SubscriptionsFacade(
new SubscriptionDataService<ApplicationDbContext, ApplicationUser>
(HttpContext.GetOwinContext().Get<ApplicationDbContext>()),
new SubscriptionProvider(ConfigurationManager.AppSettings["StripeApiSecretKey"
new CardProvider(ConfigurationManager.AppSettings["StripeApiSecretKey"],
new CardDataService<ApplicationDbContext, ApplicationUser>(Request.GetOwin
new CardDataService<ApplicationDbContext, ApplicationUser>(Request.GetOwinCont
new CustomerProvider(ConfigurationManager.AppSettings["StripeApiSecretKey"]),
new SubscriptionPlanDataService<ApplicationDbContext, ApplicationUser>(Request
new ChargeProvider(ConfigurationManager.AppSettings["StripeApiSecretKey"])));
}
}
// GET: Notes
public async Task<ActionResult> Index()
{
var userId = User.Identity.GetUserId();
var userNotes =
await
_db.Users.Where(u => u.Id == userId)
.Include(u => u.Notes)
.SelectMany(u => u.Notes)
.ToListAsync();

return View(userNotes);

// GET: Notes/Details/5
public async Task<ActionResult> Details(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
var userId = User.Identity.GetUserId();
ICollection<Note> userNotes = (
await _db.Users.Where(u => u.Id == userId)
.Include(u => u.Notes).Select(u => u.Notes)

042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091

.FirstOrDefaultAsync());
if (userNotes == null)
{
return HttpNotFound();
}

Note note = userNotes.FirstOrDefault(n => n.Id == id);


if (note == null)
{
return HttpNotFound();
}
return View(note);

// GET: Notes/Create
public ActionResult Create()
{
return View();
}

// POST: Notes/Create
// To protect from overposting attacks, please enable the specific properties you want to
// more details see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Create([Bind(Include = "Id,Title,Text,CreatedAt")] Note no
{
if (ModelState.IsValid)
{
if (await UserHasEnoughSpace(User.Identity.GetUserId()))
{
note.CreatedAt = DateTime.UtcNow;

// The note is added to the user object so the Foreign Key is saved too
var userId = User.Identity.GetUserId();
var user = await this._db.Users.Where(u => u.Id == userId).FirstOrDefaultAsync
user.Notes.Add(note);
await _db.SaveChangesAsync();
return RedirectToAction("Index");

}
else
{

TempData.Add("flash", new FlashWarningViewModel("You can not add more notes, u

notes."));
}

return View(note);

private async Task<bool> UserHasEnoughSpace(string userId)


{
var subscription = (await SubscriptionsFacade.UserActiveSubscriptionsAsync(userId)).Fi
if (subscription == null)
{
return false;

092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141

var userNotes = await _db.Users.Where(u => u.Id == userId).Include(u => u.Notes).Selec

return subscription.SubscriptionPlan.GetPropertyInt("MaxNotes") > userNotes;

// GET: Notes/Edit/5
public async Task<ActionResult> Edit(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
if (!await NoteBelongToUser(User.Identity.GetUserId(), noteId: id.Value))
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}

Note note = await _db.Notes.FindAsync(id);


if (note == null)
{
return HttpNotFound();
}
return View(note);

// POST: Notes/Edit/5
// To protect from overposting attacks, please enable the specific properties you want to
// more details see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Edit([Bind(Include = "Id,Title,Text,CreatedAt")] Note note
{
if (ModelState.IsValid && await NoteBelongToUser(User.Identity.GetUserId(), note.Id))
{
_db.Entry(note).State = EntityState.Modified;
await _db.SaveChangesAsync();
return RedirectToAction("Index");
}
return View(note);
}
// GET: Notes/Delete/5
public async Task<ActionResult> Delete(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
if (!await NoteBelongToUser(User.Identity.GetUserId(), noteId: id.Value))
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
Note note = await _db.Notes.FindAsync(id);
if (note == null)
{
return HttpNotFound();

142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191

}
return View(note);

// POST: Notes/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<ActionResult> DeleteConfirmed(int id)
{
if (!await NoteBelongToUser(User.Identity.GetUserId(), noteId: id))
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
Note note = await _db.Notes.FindAsync(id);
_db.Notes.Remove(note);
await _db.SaveChangesAsync();
return RedirectToAction("Index");

}
private async Task<bool> NoteBelongToUser(string userId, int noteId)
{
return await _db.Users.Where(u => u.Id == userId).Where(u => u.Notes.Any(n => n.Id ==
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
_db.Dispose();
}
base.Dispose(disposing);
}
}

192
193
194
195
196
197
198
199
This is itwe have the core functionality for our SAAS application.

8. Saving Customer's Location for European VAT Purposes


At the beginning of this year the legislation in the European Union for VAT for business supplying digital services
to private consumers changed. The main difference is that businesses have to charge VAT to private customers,
not business customers with a valid VAT number, according to the country in the EU in which they are based. To
validate in which country they're based we need to keep a record of at least two of these forms:

the billing address of the customer

the Internet Protocol (IP) address of the device used by the customer

customers bank details

the country code of the SIM card used by the customer

the location of the customers fixed land line through which the service is supplied

other commercially relevant information (for example, product coding information which electronically
links the sale to a particular jurisdiction)

For this reason we are going to geo-locate the user IP address, to save it along with the billing address and
credit card country.

8.1 IP Address Geo-Location


For geo-location, I am going to use Maxmind GeoLite2. It's a free database that gives us the country where an
IP is located.

Download, and add the database to App_Data, as you can see in the photo:

Install from NuGet

MaxMind.GeoIP2.
Create Extensions/GeoLocationHelper.cs.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

public static class GeoLocationHelper


{
// ReSharper disable once InconsistentNaming
/// <summary>
/// Gets the country ISO code from IP.
/// </summary>
/// <param name="ipAddress">The ip address.</param>
/// <returns></returns>
public static string GetCountryFromIP(string ipAddress)
{
string country;
try
{
using (
var reader =
new DatabaseReader(HttpContext.Current.Server.MapPath("~/App_Data/GeoLite2{
var response = reader.Country(ipAddress);
country = response.Country.IsoCode;
}
}
catch (Exception ex)
{
country = null;
}
return country;
}
/// <summary>
/// Selects the list countries.
/// </summary>

27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54

/// <param name="country">The country.</param>


/// <returns></returns>
public static List<SelectListItem> SelectListCountries(string country)
{
var getCultureInfo = CultureInfo.GetCultures(CultureTypes.SpecificCultures);
var countries =
getCultureInfo.Select(cultureInfo => new RegionInfo(cultureInfo.LCID))
.Select(getRegionInfo => new SelectListItem
{
Text = getRegionInfo.EnglishName,
Value = getRegionInfo.TwoLetterISORegionName,
Selected = country == getRegionInfo.TwoLetterISORegionName
}).OrderBy(c => c.Text).DistinctBy(i => i.Text).ToList();
return countries;
}

public static IEnumerable<TSource> DistinctBy<TSource, TKey>(this IEnumerable<TSource> sourc


{
var seenKeys = new HashSet<TKey>();
return source.Where(element => seenKeys.Add(keySelector(element)));
}
}

There are two methods implemented in this static class:

GetCountryFromIP : Returns the country ISO Code given an IP Address.

SelectListCountries : Returns a list of countries to use in a drop-down field. It has the country ISO

Code as a value for each country and the country name to be displayed.

8.2 Saving Customer Country on Registration


In the action Register from AccountController , when creating the user, save the IP and the country the IP
belongs to:
01
02
03
04
05
06
07

var userIP = GeoLocation.GetUserIP(Request);


var user = new ApplicationUser
{
UserName = model.Email,
Email = model.Email,
RegistrationDate = DateTime.UtcNow,
LastLoginTime = DateTime.UtcNow,
IPAddress = userIP,
IPAddressCountry = GeoLocationHelper.GetCountryFromIP(userIP),

08
09
10

};

Also, when we create the subscription in Stripe, we need to pass the Tax Percentage for this customer. We do
that a few lines after creating the user:
1
2
3
4

// Create Stripe user


var taxPercent = EuropeanVat.Countries.ContainsKey(user.IPAddressCountry) ?
EuropeanVat.Countries[user.IPAddressCountry] : 0;
await SubscriptionsFacade.SubscribeUserAsync(user, model.SubscriptionPlan, taxPercent:
taxPercent);

By default, if a user is based in the European Union, I'm setting the tax percentage to that subscription. The
rules are a bit more complex than that, but summarizing:

If your business is registered in an EU country, you always charge VAT to customers in your country.

If your business is registered in an EU country, you only charge VAT to the customers that are in other
EU countries, and are not VAT-registered business.

If your business is registered outside the EU, you only charge VAT to customers that are not businesses
with a valid VAT number.

8.3 Adding a Billing Address to our Model


At the moment we are not allowing to our customers to save a Billing Address, and their VAT number if they are
an EU VAT registered business. In that case, we need to change their tax percentage to 0.
SAAS Ecom provides the BillingAddress class, but it's not attached to any entity of the model. The main
reason for this is that in some SAAS applications it might make sense to assign this to an Organization class if
multiple users have access to the same account. If this is not the case, as in our sample, we can safely add that
relationship to the ApplicationUser class:
01
02
03
04
05
06
07
08
09
10
11
12
13

public class ApplicationUser : SaasEcomUser


{
public virtual ICollection<Note> Notes { get; set; }
public virtual BillingAddress BillingAddress { get; set; }

public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser> ma


{
// Note the authenticationType must match the one defined in
CookieAuthenticationOptions.AuthenticationType
var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.A
// Add custom user claims here
return userIdentity;
}
}

14
As each time that we modify the model, we need to add a database migration, openTools > NuGet Package
Manager > Package Manager Console:
1

PM> add-migration BillingAddressAddedToUser

And this is the migration class that we get:


01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

public partial class BillingAddressAddedToUser : DbMigration


{
public override void Up()
{
AddColumn("dbo.AspNetUsers", "BillingAddress_Name", c => c.String());
AddColumn("dbo.AspNetUsers", "BillingAddress_AddressLine1", c => c.String());
AddColumn("dbo.AspNetUsers", "BillingAddress_AddressLine2", c => c.String());
AddColumn("dbo.AspNetUsers", "BillingAddress_City", c => c.String());
AddColumn("dbo.AspNetUsers", "BillingAddress_State", c => c.String());
AddColumn("dbo.AspNetUsers", "BillingAddress_ZipCode", c => c.String());
AddColumn("dbo.AspNetUsers", "BillingAddress_Country", c => c.String());
AddColumn("dbo.AspNetUsers", "BillingAddress_Vat", c => c.String());
}
public override void Down()
{
DropColumn("dbo.AspNetUsers",
DropColumn("dbo.AspNetUsers",
DropColumn("dbo.AspNetUsers",
DropColumn("dbo.AspNetUsers",
DropColumn("dbo.AspNetUsers",
DropColumn("dbo.AspNetUsers",
DropColumn("dbo.AspNetUsers",
DropColumn("dbo.AspNetUsers",
}

"BillingAddress_Vat");
"BillingAddress_Country");
"BillingAddress_ZipCode");
"BillingAddress_State");
"BillingAddress_City");
"BillingAddress_AddressLine2");
"BillingAddress_AddressLine1");
"BillingAddress_Name");

To create these changes in the database, we execute in the Package Manager Console:
1

PM> update-database

One more detail that we need to fix is that in AccountController > Register, we need to set a default billing
address as it's a non-nullable field.
01
02
03
04
05
06
07
08
09
10

var user = new ApplicationUser


{
UserName = model.Email,
Email = model.Email,
RegistrationDate = DateTime.UtcNow,
LastLoginTime = DateTime.UtcNow,
IPAddress = userIP,
IPAddressCountry = GeoLocationHelper.GetCountryFromIP(userIP),
BillingAddress = new BillingAddress()
};

In the billing page, we need to display the Billing Address for the customer if it has been added, and also allow
our customers to edit it. First, we need to modify the action Index from BillingController to pass the billing
address to the view:
01
02
03
04
05
06
07
08
09
10
11

public async Task<ViewResult> Index()


{
var userId = User.Identity.GetUserId();
ViewBag.Subscriptions = await SubscriptionsFacade.UserActiveSubscriptionsAsync(userId);
ViewBag.PaymentDetails = await SubscriptionsFacade.DefaultCreditCard(userId);
ViewBag.Invoices = await InvoiceDataService.UserInvoicesAsync(userId);
ViewBag.BillingAddress = (await UserManager.FindByIdAsync(userId)).BillingAddress;
return View();
}

To display the address, we just need to edit the view "Billing/Index.cshtml", and add the view partial provided by
SAAS Ecom for that:
1
2
3
4
5
6
7
8
9

<h2>Billing</h2>
<br />
@Html.Partial("_Subscriptions", (List<Subscription>)ViewBag.Subscriptions)
<br/>
@Html.Partial("_PaymentDetails", (CreditCard)ViewBag.PaymentDetails)
<br />
@Html.Partial("_BillingAddress", (BillingAddress)ViewBag.BillingAddress)
<br />
@Html.Partial("_Invoices", (List<Invoice>)ViewBag.Invoices)

Now, if we navigate to Billing we can see the new section:

The next step is on the BillingController > BillingAddress action, we need to pass the Billing address to the view.
Because we need to get the user's two-letter ISO country code, I've added a dropdown to select the country,
which defaults to the country that the user IP belongs to:
01
02
03
04
05
06
07
08
09
10

public async Task<ViewResult> BillingAddress()


{
var model = (await UserManager.FindByIdAsync(User.Identity.GetUserId())).BillingAddress;

// List for dropdown country select


var userCountry = (await UserManager.FindByIdAsync(User.Identity.GetUserId())).IPAddressCou
ViewBag.Countries = GeoLocationHelper.SelectListCountries(userCountry);
return View(model);
}

When the user submits the form, we need to save the billing address and update the tax percent if it's needed:
01
02
03

[HttpPost]
public async Task<ActionResult> BillingAddress(BillingAddress model)
{
if (ModelState.IsValid)

04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

{
var userId = User.Identity.GetUserId();
// Call your service to save the billing address
var user = await UserManager.FindByIdAsync(userId);
user.BillingAddress = model;
await UserManager.UpdateAsync(user);

// Model Country has to be 2 letter ISO Code


if (!string.IsNullOrEmpty(model.Vat) && !string.IsNullOrEmpty(model.Country) &&
EuropeanVat.Countries.ContainsKey(model.Country))
{
await UpdateSubscriptionTax(userId, 0);
}
else if (!string.IsNullOrEmpty(model.Country) && EuropeanVat.Countries.ContainsKey(model
{
await UpdateSubscriptionTax(userId, EuropeanVat.Countries[model.Country]);
}

TempData.Add("flash", new FlashSuccessViewModel("Your billing address has been saved.")

return RedirectToAction("Index");

return View(model);

private async Task UpdateSubscriptionTax(string userId, decimal tax)


{
var user = await UserManager.FindByIdAsync(userId);
var subscription = (await SubscriptionsFacade.UserActiveSubscriptionsAsync(userId)).FirstOr
if (subscription != null && subscription.TaxPercent != tax)
{
await SubscriptionsFacade.UpdateSubscriptionTax(user, subscription.StripeId, tax);
}
}

This is what the form to add or edit a billing address looks like:

After adding the address, I get redirected back to the billing area:

As you can see in the screenshot above, because I set my country to United Kingdom, and I didn't enter a VAT
number, 20% VAT is added to the monthly price. The code showed here is assuming that you are a non-EUbased company. If that's the case, you need to handle the case where your customer is in your country,
and regardless of whether they have VAT or not, you'll have to charge VAT.

9. Deploy to Azure Websites (Web Hosting + SSL Free, SQL


Database $5 Per Month)
Advertisement

9.1 Deploying the Website


Our SAAS project is ready to go live, and I've chosen Azure as the hosting platform. If you don't have an
account yet, you can get a free trial for a month. We can deploy our app from Git (GitHub or BitBucket) on every
commit if we like. I'm going to show you here how to deploy from Visual Studio 2013. In the solution explorer,
right click on the project My Notes and select Publish from the context menu. The Publish Web wizard opens.

Select Microsoft Azure Websites and click New.

Fill in the details for your website and click Create. When your website has been created, you'll see this.
Click Next.

In this step you can add the connection string for your Database if you have it, or you can add it later from the
management portal. Click Next.

Now, if we click Publish, Visual Studio will upload the website to Azure.

9.2 Deploying the Database


To create the database, you have to go to Azure Management Portal, selectBrowse, and then Data + Storage
> SQL Database. Fill in the form to create your database.

Once the database is created, select Open in Visual Studio and accept to add an exception to the firewall.

Your database will be open in the SQL Server Object Explorer from Visual Studio. As you can see there are no
tables yet:

To generate a SQL Script to create the tables in the database, open Package Manager Console in Visual
Studio, and type:
1

PM> update-database -SourceMigration:0 -Script

Copy the script, and back in SQL Server Object Explorer, right-click on your database, and select New Query.
Paste the script, and execute it.

This script doesn't include the data that we were inserting in the database from the Seed method. We need to
create a script manually to add that data to the database:
01
02
03
04
05
06
07
08
09
10
11
12
13

INSERT INTO [dbo].[SubscriptionPlans] ([Id],[Name],[Price],[Currency],[Interval],[TrialPeriodInD


VALUES('basic_monthly', 'Basic', 10, 'USD', 1, 30, 0)
INSERT INTO [dbo].[SubscriptionPlans] ([Id],[Name],[Price],[Currency],[Interval],[TrialPeriodInD
VALUES('professional_monthly', 'Professional', 20, 'USD', 1, 30, 0)
INSERT INTO [dbo].[SubscriptionPlans] ([Id],[Name],[Price],[Currency],[Interval],[TrialPeriodInD
VALUES('business_monthly', 'Business', 30, 'USD', 1, 30, 0)
INSERT INTO [dbo].[SubscriptionPlanProperties] ([Key],[Value],[SubscriptionPlan_Id])
VALUES ('MaxNotes', '100', 'basic_monthly')
INSERT INTO [dbo].[SubscriptionPlanProperties] ([Key],[Value],[SubscriptionPlan_Id])
VALUES ('MaxNotes', '10000', 'professional_monthly')
INSERT INTO [dbo].[SubscriptionPlanProperties] ([Key],[Value],[SubscriptionPlan_Id])
VALUES ('MaxNotes', '1000000', 'business_monthly')

At this point My Notes SAAS is live. I have configured Stripe test API keys, so you can use test credit card
details for testing if you like.

You might also like