Book Management App in Umbraco 8 - FadiZahhar/umbraco8showandtell GitHub Wiki
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
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();
}
}
}
/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; }
}
/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();
}
}
}
/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);
}
/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);
}
}
/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
/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")
<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>
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();
});
{
"dashboards": [
{
"alias": "bookDashboard",
"view": "~/App_Plugins/BookDashboard/dashboard.html",
"sections": [ "content" ],
"weight": 10
}
],
"javascript": [
"~/App_Plugins/BookDashboard/dashboard.controller.js"
]
}
/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"
]
- 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.
/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)