Utilising Umbraco to power an online beer club

A presentation at Umbraco Spark 2019 in March 2019 in Bristol, UK by Thomas Morris

Slide 1

Slide 1

Utilising Umbraco to power an online beer club.

Slide 2

Slide 2

About me 2

Slide 3

Slide 3

🌉 Live in Bristol 3

Slide 4

Slide 4

2012 Started using Umbraco in 2012 Co-organiser for umBristol 4

Slide 5

Slide 5

🍺 Like beer! 5

Slide 6

Slide 6

Recently got a puppy. 6

Slide 7

Slide 7

7

Slide 8

Slide 8

About BeerBods 8

Slide 9

Slide 9

2012 UK’s first online beer club and subscription service, established in 2012. 9

Slide 10

Slide 10

Slide 11

Slide 11

2019 Launched #BUILDACASE Reserve beers using social media and have them saved for up to 30 days. 11

Slide 12

Slide 12

Slide 13

Slide 13

What’s this talk about then? 13

Slide 14

Slide 14

Powering your own niche, and hooking that into Umbraco. 14

Slide 15

Slide 15

Some topics ■ Custom data + DB migrations ■ Admin UI with Fluidity ■ Custom dashboards ■ Scheduled tasks with Hangfire ■ Umbraco events ■ Models Builder ■ GraphQL 15

Slide 16

Slide 16

Custom data with Umbraco 16

Slide 17

Slide 17

PetaPoco - what is it? ■ A micro-ORM for .NET ■ Create your own POCOs and have them saved to the DB ■ Fluent syntax (with some Umbraco tweaks) ■ Handle transactions ■ Run SQL commands ■ NPoco for v8 17

Slide 18

Slide 18

PetaPoco object [TableName(“Subscription”)] [PrimaryKey(“SubscriptionId”)] public class Subscription { [PrimaryKeyColumn(AutoIncrement = true, Name = “PK_Subscription”)] public int SubscriptionId { get; set; } [Ignore] public Customer Customer { get; set; } public int CustomerId { get; set; } public DateTime StartDate { get; set; } public DateTime NextPaymentDate { get; set; } } public bool Active { get; set; } 18

Slide 19

Slide 19

PetaPoco commands // get reference to database _db = ApplicationContext.Current.DatabaseContext.Database; // save an object _db.Save(subscription); // delete an object _db.Delete(subscription); // query and get an object back _db.FirstOrDefault<Subscription>(“SELECT TOP 1 * FROM Subscription WHERE SubscriptionId=@0”, id); // query for a collection of objects _db.Query<Subscription>(“SELECT * FROM Subscription WHERE Active=1”); 19

Slide 20

Slide 20

What about migrations? ■ Define your POCOs using attributes as required ■ Define your migrations as classes (inherit from MigrationBase) ■ Umbraco use them for each upgrade in the installer ■ On startup, use the MigrationRunner to check and process migrations ■ Can actually be used for more than DB changes 20

Slide 21

Slide 21

Migration class [Migration(“1.6.0”, 1, MigrationNames.BeerBods)] public class AddressNotesMigration : MigrationBase { public AddressNotesMigration(ISqlSyntaxProvider sqlSyntax, ILogger logger) : base(sqlSyntax, logger) { } public override void Up() { Alter.Table(“Address”).AddColumn(“Notes”).AsString().Nullable(); } } public override void Down() { Delete.Column(“Notes”).FromTable(“Address”); } 21

Slide 22

Slide 22

In an event handler… // get all migrations already executed var migrationService = ApplicationContext.Current.Services.MigrationEntryService var migrations = migrationService.GetAll(MigrationNames.BeerBods); // get the latest migration executed var currentVersion= migrations.OrderByDescending(x => x.Version).FirstOrDefault(); // create runner var migrationsRunner = new MigrationRunner( ApplicationContext.Current.Services.MigrationEntryService, ApplicationContext.Current.ProfilingLogger.Logger, currentVersion, targetVersion, MigrationNames.BeerBods); // execute migrations migrationsRunner.Execute(_databaseContext.Database, targetVersion > currentVersion); 22

Slide 23

Slide 23

How about v8? 23

Slide 24

Slide 24

Should be better… ■ Migrations in v7 are a bit all or nothing ■ In v8, there is an execution plan ■ They run in a deterministic order ■ Idea is to transition through states ■ One-way only, i.e. no UP or DOWN ■ Ask Stephan… 24

Slide 25

Slide 25

V8 Migration public class CustomMigration : MigrationBase { public CustomMigration(IMigrationContext context) : base(context) { } public override void Migrate() { Logger.Debug<CustomMigration>(“Running my CustomMigration”); if (ColumnExists(“myTable”, “myColumn”)) { Delete.Column(“myColumn”).FromTable(“myTable”).Do(); } } } Database.Execute(Sql(“DELETE FROM myOtherTable”)); 25

Slide 26

Slide 26

Viewing custom data 26

Slide 27

Slide 27

Fluidity ■ Back-office package by UMCO team. ■ A way to present custom data within Umbraco ■ Fits within the Umbraco UI ■ Can make use of Umbraco property editors (and define your own) ■ Define list/editor views via a Fluent syntax ■ Can get a fully working CRUD experience quickly 27

Slide 28

Slide 28

28

Slide 29

Slide 29

29

Slide 30

Slide 30

Set up configuration public class FluidityBootstrap : FluidityConfigModule { public override void Configure(FluidityConfig config) { // Define options for Fluidity config.AddSection(“Database”, “icon-server-alt”, sectionConfig => { sectionConfig.SetTree(“Database”, treeConfig => { } } }); }); treeConfig.AddFolder(“Subscription Info”, folderConfig => { folderConfig.AddCollection(SubscriptionFluidityConfig()); folderConfig.AddCollection(Beer13FluidityConfig()); }); 30

Slide 31

Slide 31

Create a collection config var collectionConfig = new FluidityCollectionConfig<Customer>( p => p.CustomerId, “Customer”, “Customers”, string.Empty, “icon-male-and-female”, “icon-users”); 31

Slide 32

Slide 32

Set up a collection config collectionConfig.SetNameProperty(p => p.Email); collectionConfig.SetViewMode(FluidityViewMode.List); collectionConfig.AddSearchableProperty(p => p.FirstName); collectionConfig.AddSearchableProperty(p => p.LastName); 32

Slide 33

Slide 33

Add list view collectionConfig.ListView(listViewConfig => { listViewConfig.AddField(p => p.FirstName); listViewConfig.AddField(p => p.LastName); listViewConfig.AddBulkAction<SendInviteBulkAction>(); }); listViewConfig.AddDataView(“With email”, p => p.Email != string.Empty); listViewConfig.AddDataView(“Without email”, p => p.Email == string.Empty); 33

Slide 34

Slide 34

34

Slide 35

Slide 35

Set up editor view collectionConfig.Editor(editorConfig => { editorConfig.AddTab(“Customer”, tabConfig => { tabConfig.AddField(p => p.FirstName).MakeRequired(); tabConfig.AddField(p => p.LastName).MakeRequired(); tabConfig.AddField(p => p.Email).MakeRequired(); tabConfig.AddField(p => p.Phone); tabConfig.AddField(p => p.AddressId).SetDataType(AddressPicker); tabConfig.AddField(p => p.Token).MakeReadOnly(); }); }); 35

Slide 36

Slide 36

How about v8? 36

Slide 37

Slide 37

Not yet planned… ■ Ask Matt Brailsford ■ Support UMCO ■ Chat with HQ? ■ Be more Matt Brailsford? 37

Slide 38

Slide 38

Custom dashboard ■ Just a bit of HTML/CSS/JS ■ Define a package.manifest ■ Integrate with admin controller for processing actions ■ Keep them inline with Umbraco styles ■ Build what works for you 38

Slide 39

Slide 39

Example custom dashboard

Slide 40

Slide 40

What you’ll need ■ dashboard.html ■ dashboard.controller.js ■ dashboard.resource.js ■ package.manifest ■ DashboardApiController.cs 40

Slide 41

Slide 41

Angular controller function DashboardController($scope, beerbodsDashboardResource) { var vm = this; beerbodsDashboardResource.getSummary().then(function (response) { } }); // load info vm.summaryData = response.data.details; angular.module(“umbraco”).controller(“DashboardController”, DashboardController); 41

Slide 42

Slide 42

Angular resource angular.module(‘umbraco.resources’).factory(‘beerbodsDashboardResource’, function($q, $http) { var serviceRoot = ‘backoffice/beerbods/dashboardapi/’; ); } return { getSummary: function () { return $http.get(serviceRoot } };

  • ‘getsummary’); 42

Slide 43

Slide 43

C# controller [JsonCamelCaseFormatter] [IsBackOffice, PluginController(“BeerBods”)] public class DashboardApiController : UmbracoAuthorizedApiController { [HttpGet] public HttpResponseMessage GetSummary() { var model = new DashboardViewModel() { // add details }; } } return Request.CreateResponse(HttpStatusCode.OK, new { status = “Summary info updated”, details = model }); 43

Slide 44

Slide 44

HTML file <div ng-controller=”BeerBods.DashboardController as vm”> <h3> <a href=”/umbraco/#/database/fluidity/list/order” class=”link”>Orders</a> </h3> <p> View payments on Stripe: <a href=”https://dashboard.stripe.com/”>https://dashboard.stripe.com/</a> </p> <div class=”dash-flex”> <div class=”stat-block”> <div class=”stat-block__inner”> <h4 class=”text-1”>Orders today</h4> <p class=”text-2”>{{vm.summaryData.orders.placed}}</p> </div> </div> </div> </div> 44

Slide 45

Slide 45

Build to meet a requirement. 45

Slide 46

Slide 46

47

Slide 47

Slide 47

Scheduled tasks 48

Slide 48

Slide 48

Hangfire ■ Background jobs for .NET ■ Uses persistent storage (e.g. SQL Server, Redis) ■ Can define certain times to run ■ Or trigger fire and forget commands ■ Retry policies, should provide the same result each time ■ Run outside of Umbraco context ■ Dashboard for viewing all your scheduled tasks 49

Slide 49

Slide 49

Job options // run job every 2 mins RecurringJob.AddOrUpdate(() => VerifyMembersTask.Instance.Run(), Cron.MinuteInterval(2)); // run job every hour RecurringJob.AddOrUpdate(() => ExpiredCaseTask.Instance.Run(), Cron.HourInterval(1)); // send reminders daily at 10am RecurringJob.AddOrUpdate(() => SubscriptionRemindersTask.Instance.Run(), Cron.Daily(10)); // queue a job BackgroundJob.Enqueue<ProcessOrderTask>(x => x.Run(orderId)); // queue a job on delay BackgroundJob.Schedule<NewCaseTask>(x => x.Run(caseId), TimeSpan.FromMinutes(30)); 50

Slide 50

Slide 50

51

Slide 51

Slide 51

52

Slide 52

Slide 52

Umbraco events 53

Slide 53

Slide 53

Event Handlers ■ Hook into the Umbraco pipeline ■ Saving, Saved, Publishing, Published, Deleting, Copying, etc. ■ Run your own code ■ Think about performance, keep them single use ■ e.g. default data, save product to Stripe, trigger index to Algolia ■ Web hooks for Headless 54

Slide 54

Slide 54

Add an event handler public class SaveProductEventHandler : ApplicationEventHandler { protected override void ApplicationStarted( UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) { ContentService.Saving += ContentService_Saving; } private static void ContentService_Saving(IContentService sender, SaveEventArgs<IContent> e) { // do checks… } } foreach (var content in e.SavedEntities) { // do stuff… } 55

Slide 55

Slide 55

How about v8? 56

Slide 56

Slide 56

Composers ■ A new way of doing things for v8 ■ Components are created during composition ■ Define a component that implements IComponent ■ Add via composer ■ All sorts of ‘collections’ tied to the composition ■ e.g. add your own health checks, content finders, etc. ■ Ask Stephan… 57

Slide 57

Slide 57

Set up your component public class MyComponent : IComponent { // initialize: runs once when Umbraco starts public void Initialize() { ContentService.Saving += ContentService_Saving; } // terminate: runs once when Umbraco stops public void Terminate() { } e) } private void ContentService_Saving(IContentService sender, ContentSavingEventArgs { } // Same as before 58

Slide 58

Slide 58

Set up your own composer [RuntimeLevel(MinLevel = RuntimeLevel.Run)] public class MyComposer : IUserComposer { public void Compose(Composition composition) { // Append our component to the collection of Components // It will be the last one to be run composition.Components().Append<MyComponent>(); } } 59

Slide 59

Slide 59

Powering the Front End 60

Slide 60

Slide 60

Models Builder ■ Strongly typed models for Umbraco ■ Generate on saving ■ Work nicely with compositions (generates an interface) ■ A few different ways to do it ■ Integrate with your own code ■ How to use custom view models ■ Enhance with your own data, e.g. beer ratings 61

Slide 61

Slide 61

Add these to your web.config <add <add <add <add <add key=”Umbraco.ModelsBuilder.Enable” value=”true” /> key=”Umbraco.ModelsBuilder.ModelsMode” value=”AppData” /> key=”Umbraco.ModelsBuilder.ModelsNamespace” value=”Project.Web.Core.Models.Content” /> key=”Umbraco.ModelsBuilder.ModelsDirectory” value=”~/../Project.Web.Core/Models/Content/” /> key=”Umbraco.ModelsBuilder.AcceptUnsafeModelsDirectory” value=”true” /> Other modes: ■ PureLive models (default, in memory) ■ Dll models (generates a Dll) ■ AppData models (the example above, get generated models) ■ API models (using VS extension) 62

Slide 62

Slide 62

Define a class that inherits from generated model. public class ProductBeerViewModel : ProductBeer { public ProductBeerViewModel(IPublishedContent content) : base(content) { } // custom properties } public RatingViewModel Rating { get; set; } public bool HasRating => Rating != null; In your view… @inherits UmbracoViewPage<ProductBeerViewModel> 63

Slide 63

Slide 63

Provide a custom controller public class ProductBeerController : BasePageController { private readonly RateBeerService _rateBeerService; public ProductBeerController() { _rateBeerService = new RateBeerService(); } public async Task<ActionResult> ProductBeer() { var model = new ProductBeerViewModel(CurrentPage); model.Rating = await _rateBeerService.GetRating(model.RateBeerId); } } return CurrentTemplate(model); 64

Slide 64

Slide 64

Cache the external response private IRuntimeCacheProvider runtimeCache => ApplicationContext.ApplicationCache.RuntimeCache; // fetch from cache var cacheKey = “BeerRating” + model.RateBeerId; var rating = _runtimeCache.GetCacheItem<RatingViewModel>(cacheKey); if (rating == null) { // get rating rating = await _rateBeerService.GetRating(model.RateBeerId); _runtimeCache.InsertCacheItem<RatingViewModel>(cacheKey, () => rating); } model.Rating = rating; 65

Slide 65

Slide 65

Define features, use descriptive language. 66

Slide 66

Slide 66

GraphQL ■ Query language created by Facebook ■ For using with APIs ■ Define what you want returned ■ Can be backed by different data sources ■ Viewer (GraphiQL) allows for exploration of the API 67

Slide 67

Slide 67

68

Slide 68

Slide 68

69

Slide 69

Slide 69

Algolia 70

Slide 70

Slide 70

Algolia - what is it? ■ Search as a service ■ Feed it with JSON / simple objects ■ Provide FE components for shop ui ■ Offload the demands to another service ■ Built using VueJS ■ All sorts of search options ■ Good fit for Headless, Hybrid approach 71

Slide 71

Slide 71

72

Slide 72

Slide 72

73

Slide 73

Slide 73

74

Slide 74

Slide 74

Setting up Vue import Vue from ‘vue’; import InstantSearch from ‘vue-instantsearch’; Vue.use(InstantSearch); // custom components import BuildCase from ‘./components/buildCase.vue’; import ShopListing from ‘./components/shopListing.vue’; new Vue({ el: ‘#shop’, components: { BuildCase, ShopListing } }); 75

Slide 75

Slide 75

In your Razor view… <section id=”shop” class=”shop”> <build-case> <div class=”build-case__loading”> <h4>Loading…</h4> </div> </build-case> <div id=”results” class=”container”> <shop-listing app-id=”@AlgoliaConfig.AppId” api-key=”@AlgoliaConfig.SearchKey” index-name=”@AlgoliaConfig.IndexName”> <div class=”shop-listing__loading”> <h4>Loading…</h4> <p>This is the default state.</p> </div> </shop-listing> </div> </section> 76

Slide 76

Slide 76

Choose what works for you. 77

Slide 77

Slide 77

Summary 78

Slide 78

Slide 78

■ Ways to extend and handle your own code ■ Umbraco is the glue ■ Custom data, events, tasks ■ Consistent approach that is easier to understand ■ Possibilities are endless. 79

Slide 79

Slide 79

Thanks!

Slide 80

Slide 80

UMBSPARK19 beerbods.co.uk 81