Book Management App in Umbraco 8 - FadiZahhar/umbraco8showandtell GitHub Wiki

Book Management App in Umbraco 8 – Advanced Story-Driven Tutorial


🚀 Project Brief

Goal: Build a Book Management feature inside Umbraco 8, using a dedicated SQL table, a full C# repository/service/API stack, and a dynamic backoffice dashboard using AngularJS.

You’ll learn:

  • Advanced Umbraco 8 patterns (DI, Composer, custom API, NPoco repo/service)
  • AngularJS dashboard in the backoffice
  • Modern CRUD with real business logic

1. Database Table Migration (NPoco)

The DBA creates a new table for storing books, separate from content nodes.

/App_Code/BooksTableMigration.cs

using Umbraco.Core;
using Umbraco.Core.Composing;
using Umbraco.Core.Migrations;
using Umbraco.Core.Migrations.Upgrade.V_8_0_0;

[RuntimeLevel(MinLevel = RuntimeLevel.Run)]
public class BooksTableComposer : IUserComposer
{
    public void Compose(Composition composition)
    {
        composition.Components().Append<BooksTableComponent>();
    }
}
public class BooksTableComponent : IComponent
{
    public void Initialize()
    {
        var migrationPlan = new MigrationPlan("BooksTable");
        migrationPlan.From(string.Empty)
            .To<BooksTableMigration>("books-table-created");

        var upgrader = new Upgrader(migrationPlan);
        upgrader.Execute(
            Current.Factory.GetInstance<Umbraco.Core.Persistence.IScopeProvider>(),
            Current.Factory.GetInstance<Umbraco.Core.Logging.ILogger>()
        );
    }
    public void Terminate() { }
}

public class BooksTableMigration : MigrationBase
{
    public BooksTableMigration(IMigrationContext context) : base(context) { }
    public override void Migrate()
    {
        if (!TableExists("Books"))
        {
            Create.Table("Books")
                .WithColumn("Id").AsInt32().PrimaryKey("PK_Books").Identity()
                .WithColumn("Title").AsString(255)
                .WithColumn("Author").AsString(255)
                .WithColumn("Description").AsString(2000)
                .WithColumn("CoverUrl").AsString(500)
                .Do();
        }
    }
}

2. Book Model

/App_Code/Models/BookModel.cs

public class BookModel
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Author { get; set; }
    public string Description { get; set; }
    public string CoverUrl { get; set; }
}

3. Book Repository (NPoco)

/App_Code/Repositories/BookRepository.cs

using NPoco;
using Umbraco.Core.Persistence;
using Umbraco.Core.Scoping;
using System.Collections.Generic;
using System.Linq;

public interface IBookRepository
{
    IEnumerable<BookModel> GetAll();
    BookModel GetById(int id);
    void Add(BookModel book);
    void Update(BookModel book);
    void Delete(int id);
}

public class BookRepository : IBookRepository
{
    private readonly IScopeProvider _scopeProvider;
    public BookRepository(IScopeProvider scopeProvider) => _scopeProvider = scopeProvider;

    public IEnumerable<BookModel> GetAll()
    {
        using (var scope = _scopeProvider.CreateScope())
        {
            return scope.Database.Fetch<BookModel>("SELECT * FROM Books");
        }
    }
    public BookModel GetById(int id)
    {
        using (var scope = _scopeProvider.CreateScope())
        {
            return scope.Database.Fetch<BookModel>("SELECT * FROM Books WHERE Id = @0", id).FirstOrDefault();
        }
    }
    public void Add(BookModel book)
    {
        using (var scope = _scopeProvider.CreateScope())
        {
            scope.Database.Insert("Books", "Id", false, book);
            scope.Complete();
        }
    }
    public void Update(BookModel book)
    {
        using (var scope = _scopeProvider.CreateScope())
        {
            scope.Database.Update("Books", "Id", book);
            scope.Complete();
        }
    }
    public void Delete(int id)
    {
        using (var scope = _scopeProvider.CreateScope())
        {
            scope.Database.Execute("DELETE FROM Books WHERE Id = @0", id);
            scope.Complete();
        }
    }
}

4. Book Service (Business Logic)

/App_Code/Services/BookService.cs

using System.Collections.Generic;

public interface IBookService
{
    IEnumerable<BookModel> GetAll();
    BookModel GetById(int id);
    void Add(BookModel book);
    void Update(BookModel book);
    void Delete(int id);
}

public class BookService : IBookService
{
    private readonly IBookRepository _repo;
    public BookService(IBookRepository repo) => _repo = repo;

    public IEnumerable<BookModel> GetAll() => _repo.GetAll();
    public BookModel GetById(int id) => _repo.GetById(id);
    public void Add(BookModel book) => _repo.Add(book);
    public void Update(BookModel book) => _repo.Update(book);
    public void Delete(int id) => _repo.Delete(id);
}

5. Dependency Injection Composer

/App_Code/Composers/BookComposer.cs

using Umbraco.Core;
using Umbraco.Core.Composing;

[RuntimeLevel(MinLevel = RuntimeLevel.Run)]
public class BookComposer : IUserComposer
{
    public void Compose(Composition composition)
    {
        composition.Register<IBookRepository, BookRepository>(Umbraco.Core.Composing.Lifetime.Singleton);
        composition.Register<IBookService, BookService>(Umbraco.Core.Composing.Lifetime.Singleton);
    }
}

6. Book API Controller

/App_Code/Controllers/BookApiController.cs

using System.Collections.Generic;
using System.Web.Http;
using Umbraco.Web.WebApi;

public class BookApiController : UmbracoApiController
{
    private readonly IBookService _bookService;
    public BookApiController(IBookService bookService) => _bookService = bookService;

    [HttpGet]
    public IEnumerable<BookModel> GetAll() => _bookService.GetAll();

    [HttpGet]
    public BookModel GetById(int id) => _bookService.GetById(id);

    [HttpPost]
    public void Add(BookModel model) => _bookService.Add(model);

    [HttpPost]
    public void Update(BookModel model) => _bookService.Update(model);

    [HttpPost]
    public void Delete(int id) => _bookService.Delete(id);
}

Endpoints:

  • /umbraco/api/bookapi/getall
  • /umbraco/api/bookapi/getbyid?id=1
  • /umbraco/api/bookapi/add
  • /umbraco/api/bookapi/update
  • /umbraco/api/bookapi/delete?id=1

7. (Optional) SurfaceController for Public MVC List

/App_Code/Controllers/BookSurfaceController.cs

using System.Web.Mvc;
using Umbraco.Web.Mvc;

public class BookSurfaceController : SurfaceController
{
    private readonly IBookService _service;
    public BookSurfaceController(IBookService service) => _service = service;

    public ActionResult List()
    {
        var books = _service.GetAll();
        return PartialView("~/Views/Partials/BooksList.cshtml", books);
    }
}

/Views/Partials/BooksList.cshtml

@model IEnumerable<BookModel>
<ul>
@foreach(var book in Model)
{
    <li>@book.Title by @book.Author</li>
}
</ul>

Usage in view: @Html.Action("List", "BookSurface")


8. Backoffice AngularJS Dashboard

/App_Plugins/BookDashboard/dashboard.html

<div ng-controller="BookDashboardController as vm" class="book-dashboard">
    <h2>Manage Books</h2>
    <form ng-submit="vm.saveBook()">
        <input ng-model="vm.currentBook.title" placeholder="Title" required>
        <input ng-model="vm.currentBook.author" placeholder="Author" required>
        <textarea ng-model="vm.currentBook.description" placeholder="Description"></textarea>
        <input ng-model="vm.currentBook.coverUrl" placeholder="Cover URL">
        <button type="submit">{{ vm.currentBook.id ? "Update" : "Add" }} Book</button>
    </form>
    <ul>
        <li ng-repeat="book in vm.books">
            <b>{{ book.title }}</b> by {{ book.author }}
            <button ng-click="vm.editBook(book)">Edit</button>
            <button ng-click="vm.deleteBook(book.id)">Delete</button>
        </li>
    </ul>
</div>

/App_Plugins/BookDashboard/dashboard.controller.js

angular.module("umbraco").controller("BookDashboardController", function($http) {
    var vm = this;
    vm.books = [];
    vm.currentBook = {};

    vm.loadBooks = function() {
        $http.get("/umbraco/api/bookapi/getall").then(function(res) {
            vm.books = res.data;
        });
    };

    vm.saveBook = function() {
        if(vm.currentBook.id){
            $http.post("/umbraco/api/bookapi/update", vm.currentBook).then(function(){
                vm.currentBook = {};
                vm.loadBooks();
            });
        } else {
            $http.post("/umbraco/api/bookapi/add", vm.currentBook).then(function(){
                vm.currentBook = {};
                vm.loadBooks();
            });
        }
    };

    vm.editBook = function(book){
        vm.currentBook = angular.copy(book);
    };

    vm.deleteBook = function(id){
        $http.post("/umbraco/api/bookapi/delete?id=" + id).then(function(){
            vm.loadBooks();
        });
    };

    vm.loadBooks();
});

/App_Plugins/BookDashboard/package.manifest

{
  "dashboards": [
    {
      "alias": "bookDashboard",
      "view": "~/App_Plugins/BookDashboard/dashboard.html",
      "sections": [ "content" ],
      "weight": 10
    }
  ],
  "javascript": [
    "~/App_Plugins/BookDashboard/dashboard.controller.js"
  ]
}

9. Styling (Optional)

/App_Plugins/BookDashboard/dashboard.css

.book-dashboard { margin: 20px; }
.book-dashboard input, .book-dashboard textarea { margin: 5px 0; display: block; }
.book-dashboard ul { list-style: disc inside; padding-left: 0; }
.book-dashboard li { margin: 10px 0; }

Add to package.manifest:

"css": [
  "~/App_Plugins/BookDashboard/dashboard.css"
]

10. Testing & Usage

  • Restart Umbraco/recycle app pool to pick up DI and migration.
  • Log in to the backoffice.
  • See the new Book Dashboard tab under Content.
  • Add, edit, and delete books—data is stored in your custom SQL table.
  • Use the API endpoints for integration or frontend Angular/MVC rendering.

Directory Layout (Typical)

/App_Code/
    /Models/BookModel.cs
    /Repositories/BookRepository.cs
    /Services/BookService.cs
    /Composers/BookComposer.cs
    /BooksTableMigration.cs
    /Controllers/BookApiController.cs
    /Controllers/BookSurfaceController.cs (optional)
/App_Plugins/BookDashboard/
    dashboard.html
    dashboard.controller.js
    dashboard.css
    package.manifest
/Views/Partials/
    BooksList.cshtml (optional)
⚠️ **GitHub.com Fallback** ⚠️