Utilising Umbraco to power an online beer club.
A presentation at Umbraco Spark 2019 in March 2019 in Bristol, UK by Thomas Morris
Utilising Umbraco to power an online beer club.
About me 2
đ Live in Bristol 3
2012 Started using Umbraco in 2012 Co-organiser for umBristol 4
đș Like beer! 5
Recently got a puppy. 6
7
About BeerBods 8
2012 UKâs first online beer club and subscription service, established in 2012. 9
2019 Launched #BUILDACASE Reserve beers using social media and have them saved for up to 30 days. 11
Whatâs this talk about then? 13
Powering your own niche, and hooking that into Umbraco. 14
Some topics â Custom data + DB migrations â Admin UI with Fluidity â Custom dashboards â Scheduled tasks with Hangfire â Umbraco events â Models Builder â GraphQL 15
Custom data with Umbraco 16
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
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
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
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
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
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
How about v8? 23
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
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
Viewing custom data 26
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
28
29
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
Create a collection config var collectionConfig = new FluidityCollectionConfig<Customer>( p => p.CustomerId, âCustomerâ, âCustomersâ, string.Empty, âicon-male-and-femaleâ, âicon-usersâ); 31
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
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
34
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
How about v8? 36
Not yet planned⊠â Ask Matt Brailsford â Support UMCO â Chat with HQ? â Be more Matt Brailsford? 37
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
Example custom dashboard
What youâll need â dashboard.html â dashboard.controller.js â dashboard.resource.js â package.manifest â DashboardApiController.cs 40
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
Angular resource angular.module(âumbraco.resourcesâ).factory(âbeerbodsDashboardResourceâ, function($q, $http) { var serviceRoot = âbackoffice/beerbods/dashboardapi/â; ); } return { getSummary: function () { return $http.get(serviceRoot } };
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
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
Build to meet a requirement. 45
47
Scheduled tasks 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
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
51
52
Umbraco events 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
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
How about v8? 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
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
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
Powering the Front End 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
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
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
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
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
Define features, use descriptive language. 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
68
69
Algolia 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
72
73
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
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
Choose what works for you. 77
Summary 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
Thanks!
UMBSPARK19 beerbods.co.uk 81