Use wordpress theme!
What is the business model?
- Sell videos (ie download)
- Sell access to the videos (ie stream)
- Have small chunks ie episodes
What is our model (starting in baby steps)?
- A Customer buys a Production
- A Production is one or more Episodes
- A Customer buys a Subscription
- A Subscription gives access to Productions
so episodes cannot be purchased individually.
Write Tests
- CustomerSpecs
- SubscriptionSpecs
- ProductionSpecs
- EpisodeSpecs
Share them with the team as a discussion around business logic.
[TestFixture]
public class CustomerSpecs : TestBase {
[Test]
public void a_user_should_be_able_to_add_production_to_cart() {
this.IsPending();
}
[Test]
public void a_user_that_owns_a_production_should_be_able_to_stream() {
this.IsPending();
}
[Test]
public void a_user_that_owns_a_production_should_be_able_to_download() {
this.IsPending();
}
[Test]
public void a_user_should_have_be_able_to_purchase_sub() {
this.IsPending();
}
[Test]
public void a_user_with_monthly_should_only_be_able_to_stream() {
this.IsPending();
}
[Test]
public void a_user_with_yearly_should_be_able_to_stream_and_download() {
this.IsPending();
}
[Test]
public void a_user_with_cancelled_sub_should_not_be_able_to_stream_or_download() {
this.IsPending();
}
[Test]
public void a_user_with_a_suspended_sub_should_not_be_able_to_stream_or_download() {
this.IsPending();
}
[Test]
public void a_user_with_overdue_sub_should_be_able_to_stream_or_download() {
this.IsPending();
}
[Test]
public void a_user_should_be_able_to_cancel_sub() {
this.IsPending();
}
}
//A Subscription grants access over time
//there is a monthly and a yearly
//a monthly subscription offers stream only access
//a yearly offers stream and download
//a Customer can buy a subscription
[TestFixture]
public class SubscriptionSpecs : TestBase {
[Test]
public void a_subscription_is_only_valid_one_per_user() {
this.IsPending();
}
[Test]
public void a_subscription_can_be_pending_current_overdue_suspended_or_cancelled() {
this.IsPending();
}
[Test]
public void a_pending_subscription_can_turn_current() {
this.IsPending();
}
[Test]
public void a_current_subscription_can_become_overdue_if_payment_late() {
this.IsPending();
}
[Test]
public void an_overdue_subscription_can_become_current_if_payment_received_in_full() {
this.IsPending();
}
[Test]
public void an_overdue_subscription_can_become_suspended_if_payment_not_received_after_3_tries() {
this.IsPending();
}
[Test]
public void a_subscription_can_be_upgraded_from_monthly_to_annual() {
this.IsPending();
}
[Test]
public void a_subscription_cannot_be_downgraded_from_annual_to_monthly() {
this.IsPending();
}
}
//A Production is a collection of Episodes
//A Customer can buy a Production
//A Customer cannot buy an individual episode
[TestFixture]
public class ProductionSpecs : TestBase {
[Test]
public void a_production_has_one_or_more_episodes() {
this.IsPending();
}
[Test]
public void a_production_can_cost_0_or_more_dollars() {
this.IsPending();
}
[Test]
public void a_production_can_be_in_production_published_suspended_or_offline() {
this.IsPending();
}
[Test]
public void a_production_is_viewable_if_not_offline() {
this.IsPending();
}
[Test]
public void a_production_can_be_downloaded_if_flagged() {
this.IsPending();
}
[Test]
public void episodes_can_be_released_offline_in_process() {
this.IsPending();
}
[Test]
public void episodes_are_viewable_if_released() {
this.IsPending();
}
[Test]
public void customers_can_see_notes_per_production() {
this.IsPending();
}
[Test]
public void customers_can_see_notes_per_episode() {
this.IsPending();
}
[Test]
public void customers_can_see_when_an_episode_and_production_was_released() {
this.IsPending();
}
[Test]
public void customers_can_see_who_authored_the_production() {
this.IsPending();
}
[Test]
public void customers_can_see_how_long_an_episode_is() {
this.IsPending();
}
[Test]
public void customers_can_see_total_duration_of_production() {
this.IsPending();
}
DB
DB First, Classes (or code) First, Migrations (from Rails world)
but .NET people don’t know migrations so leave for now.
EF Code-first
git checkout –b “codefirst”
EF Code-first
NuGet install in Web project.
Make classes with some properties:
public class Production {
[Required]
public string Title { get; set; }
public string Description { get; set; }
public decimal Price { get; set; }
}
Rob isn’t a fan of DataAnnotations…messy. More attributes than code… aren’t descriptive.. hard to refactor.
eg in AccountModel:
namespace VidPub.Web.Models {
public class ChangePasswordModel {
[Required]
[DataType(DataType.Password)]
[Display(Name = "Current password")]
public string OldPassword { get; set; }
[Required]
[StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)]
[DataType(DataType.Password)]
[Display(Name = "New password")]
public string NewPassword { get; set; }
[DataType(DataType.Password)]
[Display(Name = "Confirm new password")]
[Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")]
public string ConfirmPassword { get; set; }
}
public class LogOnModel {
[Required]
[Display(Name = "User name")]
public string UserName { get; set; }
[Required]
[DataType(DataType.Password)]
[Display(Name = "Password")]
public string Password { get; set; }
[Display(Name = "Remember me?")]
public bool RememberMe { get; set; }
}
setup DbContext:
namespace VidPub.Web.Models {
public class VidpubDBContext : DbContext {
public DbSet<Production> Productions { get; set; }
}
}
Trick#1 – EF will try to use .\SQLExpress
No PK defined.
Guids as PK’s make DBA’s cry?
EF Conventions will use these as PK’s:
namespace VidPub.Web.Models {
public class Production {
[Required]
public int ID { get; set; }
[Required]
public string Title { get; set; }
public string Description { get; set; }
public decimal Price { get; set; }
}
}
with no connection string defined we have our db created.
However its made the nvarchar(MAX) which isn’t good..
namespace VidPub.Web.Models {
public class Production {
[Required]
public int ID { get; set; }
[Required]
[MaxLength(200)]
public string Title { get; set; }
public string Description { get; set; }
public decimal Price { get; set; }
}
}
however on recompile:
Tip #2: EF Code-first won’t run ALTER… its drop or recreate.
FK
public class Production {
[Required]
public int ID { get; set; }
[Required]
public string Title { get; set; }
public string Description { get; set; }
public decimal Price { get; set; }
public ICollection<Episode> Episodes { get; set; }
}
public class Episode {
public int ID { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public int ProductionID { get; set; }
}
and it gen’s up the database fine.
var db = new VidPub.Web.Models.VidpubDBContext();
var p = new Production { Title = "My Production" };
var e = new Episode { Title = "My Episode" };
p.Episodes = new List<Episode>();
p.Episodes.Add(e);
db.Productions.Add(p);
db.SaveChanges();
MVC Scaffolding
git checkout –b “scaffolding”
Install-Package MvcScaffolding
only have this in the model (no dbcontext from above)
namespace VidPub.Web.Models {
public class Production {
public int ID { get; set; }
[Required]
public string Title { get; set; }
public string Description { get; set; }
public decimal Price { get; set; }
public ICollection<Episode> Episodes { get; set; }
}
public class Episode {
public int ID { get; set; }
public int ProductionID { get; set; }
public string Title { get; set; }
}
}
Scaffold Controller production
And it worked:
hmm – fast to go.. but even changes need to make eg varchar(max), database regens.. are going to get old v.soon.
Opinion
EF and Code-first… more work…in real world.
Make model in Database:
The less abstractions the better.. he loves rails activerecord.
Scaleable and Maintainable
More dev’s will know EF in the future years than anything else.
Good/Bad opinion of EF -
Massive
Bad – future hires won’t know it
Good
- small and simple
- high perf
- this has worked for Rob in the past
Controller Design
Design RESTfully
REpresentational State Trasfer
“create an experience for a user that is predictable and understandable based on a url”
so now have a controller that is stubbed out and ready to go.
public class ProductionsController : Controller
{
Productions _table;
public ProductionsController() {
_table = new Productions();
}
public ActionResult Index()
{
return View(_table.All());
}
then create a view:
fooling the tooling a bit as we’re going to be using dynamics, so chose any object.
Made db in the db! From the dbscript.sql file that came with VidPub.
So now we’ve got data reading from via Massive.
public class ProductionsController : Controller
{
Productions _table;
public ProductionsController() {
_table = new Productions();
}
public ActionResult Index()
{
return View(_table.All());
}
via Massive (vidpub connection string in web.config)
public class Productions : DynamicModel {
public Productions()
: base("VidPub", "Productions", "ID") {
}
<connectionStrings>
<add name="VidPub" connectionString="server=.\;database=VidPub_Dev;integrated security=true" />
and rendered:
Views
[HttpPost]
public ActionResult Create(FormCollection collection)
{
dynamic item = _table.CreateFrom(collection);
try
{
_table.Insert(item);
return RedirectToAction("Index");
}
catch
{
TempData["alert"] = "There was an error adding this item";
return View();
}
}
CreateFrom creates a new Expando, white listed against the columns in the db.
Customizing the Generators - T4
As we can’t do @Html.TextBox(“title”, Model.Title)… extension methods and dynamics don’t play well together.
If we do string title = Model.Title then it all works.
This added a bunch of templates":
Then just took our ProductionsController code and put it into the template Controller.tt
<#@ template language="C#" HostSpecific="True" #>
<#
MvcTextTemplateHost mvcHost = (MvcTextTemplateHost)(Host);
#>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using VidPub.Web.Models;
<#
var controllerName = mvcHost.ControllerName;
var nameSpace = mvcHost.Namespace;
var tableName = controllerName.Replace("Controller", "");
#>
namespace <#= nameSpace #> {
public class <#= controllerName #> : Controller
{
dynamic _table;
public <#= controllerName #>() {
_table = new <#= tableName #>();
ViewBag.Table = _table;
}
public ActionResult Index()
{
return View(_table.All());
}
So now it generates much more cleanly.
namespace VidPub.Web.Controllers
{
public class ProductionsController : ApplicationController
{
dynamic _table;
public ProductionsController() {
_table = new Productions();
ViewBag.Table = _table;
}
public ActionResult Index()
{
return View(_table.All());
}
public ActionResult Details(int id)
{
return View(_table.FindBy(ID: id, schema: true));
}
public ActionResult Create()
{
return View(_table.Prototype);
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(FormCollection collection)
{
dynamic item = _table.CreateFrom(collection);
try
{
_table.Insert(item);
return RedirectToAction("Index");
}
catch
{
TempData["alert"] = "There was an error adding this item";
return View();
}
}
public ActionResult Edit(int id)
{
var model = _table.Get(ID: id);
model._Table = _table;
return View(model);
}
[HttpPost]
public ActionResult Edit(int id, FormCollection collection)
{
var model = _table.CreateFrom(collection);
try
{
_table.Update(model, id);
return RedirectToAction("Index");
}
catch (Exception ex)
{
TempData["Error"] = "There was a problem editing this record";
return View(model);
}
}
public ActionResult Delete(int id)
{
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Delete(int id, FormCollection collection)
{
try
{
_table.Delete(id);
return RedirectToAction("Index");
}
catch
{
TempData["Error"] = "There was a problem deleting this record";
return View("Index");
}
}
}
}
CruddyController
abstracting to a base class the cruddyness:
namespace VidPub.Web.Infrastructure {
public class CruddyController : ApplicationController {
// IoC will need to inject a tokenStore in everything that inhertis hmmmmmm
public CruddyController(ITokenHandler tokenStore) : base(tokenStore) {}
protected dynamic _table;
//all virtual so can override if necessary
public virtual ActionResult Index() {
return View(_table.All());
}
public virtual ActionResult Details(int id) {
return View(_table.FindBy(ID: id, schema: true));
}
public virtual ActionResult Create() {
return View(_table.Prototype);
}
[HttpPost]
[ValidateAntiForgeryToken]
public virtual ActionResult Create(FormCollection collection) {
dynamic item = _table.CreateFrom(collection);
try {
_table.Insert(item);
return RedirectToAction("Index");
}
catch {
TempData["alert"] = "There was an error adding this item";
return View();
}
}
public virtual ActionResult Edit(int id) {
var model = _table.Get(ID: id);
model._Table = _table;
return View(model);
}
[HttpPost]
public virtual ActionResult Edit(int id, FormCollection collection) {
var model = _table.CreateFrom(collection);
try {
_table.Update(model, id);
return RedirectToAction("Index");
}
catch (Exception ex) {
TempData["Error"] = "There was a problem editing this record";
return View(model);
}
}
public virtual ActionResult Delete(int id) {
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
public virtual ActionResult Delete(int id, FormCollection collection) {
try {
_table.Delete(id);
return RedirectToAction("Index");
}
catch {
TempData["Error"] = "There was a problem deleting this record";
return View("Index");
}
}
}
}
Review
- Use what you know and get app to market
- Big ORM in startup can get in the way
- Build whats needed, no more
- Avoided orm here