Professional Documents
Culture Documents
75
107
Share
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.
The first thing that we need to do is open Visual Studio 2013. Create a new 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:
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
[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
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.
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.
[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.
Microsoft.AspNet.Identity.Core: This package contains the core interfaces for 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
user
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.
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.
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.
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
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);
In AccountController -> ForgotPassword action, this is the code to send the email:
1
2
3
4
You can see that some files have been added to your solution:
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>
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.
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
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);
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
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
You can simplify the way that this is instantiated using dependency injection, but this is something that can be
covered in another article.
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
10
11
12
13
14
Because the DbContext has changed, we need to add a new database Migration, so open Nuget Package
Manager console and run:
1
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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
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:
List of notes:
Note detail:
Edit note:
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
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();
}
// 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
{
notes."));
}
return View(note);
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
// 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);
}
// 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.
the Internet Protocol (IP) address of the device 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.
Download, and add the database to App_Data, as you can see in the photo:
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
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
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.
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
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.
14
As each time that we modify the model, we need to add a database migration, openTools > NuGet Package
Manager > Package Manager Console:
1
"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
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
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)
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
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);
return RedirectToAction("Index");
return View(model);
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.
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.
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
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
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.