“Book Management” app in Umbraco 8 - FadiZahhar/umbraco8showandtell GitHub Wiki

1. Create the POCO Model

Umbraco 8 prefers PetaPoco attributes on your data model, but for migrations, you mostly just need a matching class for easier use (optional for pure migration).

using NPoco;

namespace HighlyDeveloped.Core.Models
{
    [TableName("Books")]
    [PrimaryKey("Id", AutoIncrement = true)]
    public class Book
    {
        public int Id { get; set; }

        public string Title { get; set; }
        public string Author { get; set; }
        public int Year { get; set; }
        public string ISBN { get; set; }
    }
}

2. Migration Class

Follow the Umbraco migration pattern using MigrationBase, but be sure to use the recommended helpers for Umbraco 8. Avoid custom SQL for the table creation; always use the Create.Table API.

using Umbraco.Core.Migrations;

namespace HighlyDeveloped.Core.Migrations
{
    public class CreateBookTableMigration : MigrationBase
    {
        public CreateBookTableMigration(IMigrationContext context) : base(context) { }

        public override void Migrate()
        {
            if (!TableExists("Books"))
            {
                Create.Table("Books")
                    .WithColumn("Id").AsInt32().PrimaryKey("PK_Books_Id").Identity()
                    .WithColumn("Title").AsString(255).NotNullable()
                    .WithColumn("Author").AsString(255).NotNullable()
                    .WithColumn("Year").AsInt32().NotNullable()
                    .WithColumn("ISBN").AsString(50).Nullable()
                    .Do();
            }
        }
    }
}
  • NotNullable for required fields, Nullable for optional.
  • Table name is pluralized and matches your POCO.

3. Register and Run the Migration (Component and Composer)

Component:

using Umbraco.Core.Composing;
using Umbraco.Core.Migrations;
using Umbraco.Core.Scoping;
using Umbraco.Core.Services;
using Umbraco.Core.Logging;
using Umbraco.Core.Migrations.Upgrade;

namespace HighlyDeveloped.Core.Migrations
{
    public class BookTableMigrationComponent : IComponent
    {
        private readonly IScopeProvider _scopeProvider;
        private readonly IMigrationBuilder _migrationBuilder;
        private readonly IKeyValueService _keyValueService;
        private readonly ILogger _logger;

        public BookTableMigrationComponent(
            IScopeProvider scopeProvider,
            IMigrationBuilder migrationBuilder,
            IKeyValueService keyValueService,
            ILogger logger)
        {
            _scopeProvider = scopeProvider;
            _migrationBuilder = migrationBuilder;
            _keyValueService = keyValueService;
            _logger = logger;
        }

        public void Initialize()
        {
            var migrationPlan = new MigrationPlan("BookTable");
            migrationPlan.From(string.Empty)
                .To<CreateBookTableMigration>("create-book-table");
            var upgrader = new Upgrader(migrationPlan);

            upgrader.Execute(_scopeProvider, _migrationBuilder, _keyValueService, _logger);
        }

        public void Terminate() { }
    }
}

Composer:

using HighlyDeveloped.Core.Migrations;
using Umbraco.Core;
using Umbraco.Core.Composing;

namespace HighlyDeveloped.Core.Composers
{
    public class BookTableMigrationComposer : IUserComposer
    {
        public void Compose(Composition composition)
        {
            composition.Components().Append<BookTableMigrationComponent>();
        }
    }
}

4. (Recommended) Use a Repository with PetaPoco for CRUD

For CRUD operations, inject the IScopeProvider into your repository and use Database via a scope:

using HighlyDeveloped.Core.Models;
using NPoco;
using System.Collections.Generic;
using Umbraco.Core.Scoping;

namespace HighlyDeveloped.Core.Repositories
{
    public class BookRepository
    {
        private readonly IScopeProvider _scopeProvider;

        public BookRepository(IScopeProvider scopeProvider)
        {
            _scopeProvider = scopeProvider;
        }

        public IEnumerable<Book> GetAll()
        {
            using (var scope = _scopeProvider.CreateScope())
            {
                return scope.Database.Fetch<Book>("SELECT * FROM Books");
            }
        }

            public Book GetById(int id)
            {
                using (var scope = _scopeProvider.CreateScope())
                {
                    return scope.Database.SingleOrDefault<Book>("SELECT * FROM Books WHERE Id = @0", id);
                }
            }

            public void Insert(Book book)
            {
                using (var scope = _scopeProvider.CreateScope())
                {
                    scope.Database.Insert(book);
                    scope.Complete();
                }
            }

            public void Update(Book book)
            {
                using (var scope = _scopeProvider.CreateScope())
                {
                    scope.Database.Update(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();
                }
            }
        }
    }

5. Additional Recommendations

  • Always use TableExists before table creation.
  • Use strongly-typed POCOs for working with the table.
  • Register all dependencies (repositories/services) via a Composer.
  • Migration Plan name (BookTable) must be unique—don’t reuse for other tables.

Quick Troubleshooting

  • If the table doesn’t appear:

    • Check /App_Data/Logs/UmbracoTraceLog.txt for errors.
    • Ensure your DLL is in /bin and public classes/namespaces are correct.
    • Restart the site/app pool after code changes.

Reference

You can follow this approach for any custom table in Umbraco 8, based on the best practices from the official documentation.


Summary

  1. POCO with PetaPoco attributes (optional, but recommended for CRUD).
  2. Migration creates the table with Create.Table helpers.
  3. Component & Composer run the migration plan on startup.
  4. Repository/Service for CRUD using IScopeProvider.
  5. Register everything in Composers.

📚 Build a Custom Book Management Dashboard in Umbraco 8


🏁 Introduction & Goal

You’re building a Book Management App for your library client. They want to manage their book catalog directly from Umbraco’s backoffice, but outside the content tree—with a modern AngularJS dashboard. You’ll store book data in a custom database table, create a repository for database access (NPoco), expose an API for the UI, manage everything through a service, and register all with dependency injection and a composer. Finally, you’ll tie it all together with a custom dashboard in Umbraco’s UI.


🛠️ 1. Create the Book Model

File: /Models/Book.cs

This is the shape of your book object, and will also become your table schema.


🗄️ 2. Database Table Migration

File: /Migrations/CreateBookTableMigration.cs

This code runs on startup to create your custom Books table.


🏢 3. Repository: Database Layer

File: /Repositories/BookRepository.cs

All SQL is isolated here for clean separation and testability.


🧠 4. Service: Business Logic Layer

File: /Services/BookService.cs

using System.Collections.Generic;
using YourNamespace.Models;
using YourNamespace.Repositories;

namespace YourNamespace.Services
{
    public class BookService
    {
        private readonly BookRepository _repo;
        public BookService(BookRepository repo) { _repo = repo; }

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

This is where any complex business rules go. For now, it passes through to the repo.


inject the service with composer

File: `/Composers/BookServiceComposer.cs

using HighlyDeveloped.Core.Repositories;
using HighlyDeveloped.Core.Services;
using Umbraco.Core;
using Umbraco.Core.Composing;

public class BookServiceComposer : IUserComposer
{
    public void Compose(Composition composition)
    {
        composition.Register<BookService>(Lifetime.Singleton);
        // If BookRepository is a dependency, register it too!
        composition.Register<BookRepository>(Lifetime.Singleton);
    }
}

🌐 5. API Controller

File: /Controllers/BookApiController.cs

using System.Collections.Generic;
using System.Web.Http;
using Umbraco.Web.WebApi;
using YourNamespace.Models;
using YourNamespace.Services;

namespace YourNamespace.Controllers
{
    public class BookApiController : UmbracoAuthorizedApiController
    {
        private readonly BookService _service;
        public BookApiController(BookService service) { _service = service; }

        [HttpGet]
        public IEnumerable<Book> GetAll() => _service.GetAll();

        [HttpGet]
        public Book GetById(int id) => _service.GetById(id);

        [HttpPost]
        public void PostSave(Book book)
        {
            if (book.Id == 0)
                _service.Add(book);
            else
                _service.Update(book);
        }

        [HttpPost]
        public void PostDelete([FromBody] int id) => _service.Delete(id);
    }
}

All CRUD endpoints for the AngularJS UI.


🔌 6. Register Everything with a Composer

File: /Composers/BookManagerComposer.cs

using HighlyDeveloped.Core.Migrations;
using Umbraco.Core;
using Umbraco.Core.Composing;

namespace HighlyDeveloped.Core.Composers
{
    public class BookTableMigrationComposer : IUserComposer
    {
        public void Compose(Composition composition)
        {
            composition.Components().Append<BookTableMigrationComponent>();
        }
    }
}

This registers your repository, service, and ensures the migration runs.


🎛️ 7. App_Plugins: The Dashboard UI

Folder: /App_Plugins/BookManager/

package.manifest

{
  "dashboards": [
    {
      "alias": "bookManagerDashboard",
      "view": "/App_Plugins/BookManager/dashboard/dashboard.html",
      "sections": [ "content" ],
      "weight": 10,
      "label": "Book Manager"
    }
  ],
  "javascript": [
    "~/App_Plugins/BookManager/dashboard/dashboard.controller.js"
  ],
  "css": [
    "https://www.w3schools.com/w3css/5/w3.css"
  ]
}

/dashboard/dashboard.html

<div ng-controller="BookManagerDashboardController as vm">
    <h2>Book Manager</h2>
    <button class="btn" ng-click="vm.newBook()">Add Book</button>
    <table class="table">
        <tr>
            <th>Title</th>
            <th>Author</th>
            <th>Year</th>
            <th>ISBN</th>
            <th>Actions</th>
        </tr>
        <tr ng-repeat="book in vm.books">
            <td>{{book.title}}</td>
            <td>{{book.author}}</td>
            <td>{{book.year}}</td>
            <td>{{book.isbn}}</td>
            <td>
                <button class="btn btn-link" ng-click="vm.editBook(book)">Edit</button>
                <button class="btn btn-link" ng-click="vm.deleteBook(book.id)">Delete</button>
            </td>
        </tr>
    </table>

    <!-- Add/Edit Form -->
    <div ng-if="vm.editing">
        <form  novalidate>
            <label>Title: <input ng-model="vm.currentBook.title" required /></label><br>
            <label>Author: <input ng-model="vm.currentBook.author" required /></label><br>
            <label>Year: <input ng-model="vm.currentBook.year" type="number" required /></label><br>
            <label>ISBN: <input ng-model="vm.currentBook.isbn" required /></label><br>
            <button type="button" ng-click="vm.saveBook()" class="btn btn-success" ng-disabled="bookForm.$invalid">Save</button>
            <button type="button" class="btn" ng-click="vm.cancel()">Cancel</button>
        </form>
    </div>
</div>

/dashboard/dashboard.controller.js

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

        vm.loadBooks = function () {
            $http.get("/umbraco/backoffice/api/BookApi/GetAll").then(function (res) {
                vm.books = res.data;
                console.log("here", res.data);
            });
        };

        vm.newBook = function () {
            vm.editing = true;
            vm.currentBook = {};
        };

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

        vm.saveBook = function () {
            $http.post("/umbraco/backoffice/api/BookApi/PostSave", vm.currentBook).then(function () {
                vm.editing = false;
                vm.loadBooks();
            });
        };

        vm.deleteBook = function (id) {
            $http.post("/umbraco/backoffice/api/BookApi/PostDelete", id).then(function () {
                vm.loadBooks();
            });
        };

        vm.cancel = function () {
            vm.editing = false;
        };

        vm.loadBooks();
    });
---

## 🌎 8. ** Public API Route**

File: `/Controllers/BooksPublicController.cs`

```csharp
using System.Collections.Generic;
using System.Web.Http;
using Umbraco.Web.WebApi;
using YourNamespace.Models;
using YourNamespace.Services;

public class BooksPublicController : UmbracoApiController
{
    private readonly BookService _service;
    public BooksPublicController(BookService service) { _service = service; }

    [HttpGet]
    public IEnumerable<Book> GetAllBooks() => _service.GetAll();
}

Now, anyone can fetch books at /umbraco/api/BooksPublic/GetAllBooks.


🎉 You Did It!

  • Custom Controller: BookApiController (CRUD API)
  • Service: BookService (business logic)
  • Repository: BookRepository (NPoco/SQL)
  • Custom Routing: Public endpoint for books
  • Dashboard UI: Modern AngularJS interface
  • Composer: Registers DI + migration
  • Dependency Injection: Everywhere, clean code!

🆕 Advanced Features for the Book Manager


Adding validation

Update the Book Model

using System.ComponentModel.DataAnnotations;
using NPoco;

namespace HighlyDeveloped.Core.Models
{
    [TableName("Books")]
    [PrimaryKey("Id", AutoIncrement = true)]
    public class Book
    {
        public int Id { get; set; }

        [Required]
        [StringLength(150)]
        public string Title { get; set; }

        [Required]
        [StringLength(100)]
        public string Author { get; set; }

        [Range(1450, 2100)] // Reasonable year range for books
        public int Year { get; set; }

        [Required]
        [RegularExpression(@"^\d{3}-\d{10}$", ErrorMessage = "ISBN must be in the format 123-1234567890")]
        public string ISBN { get; set; }
    }
}

1. Make Sure Your Book Model Has Data Annotations

Make sure your Book model (as shown earlier) uses attributes like [Required], [StringLength], etc.


2. Enable Model Validation in Your Controller

a) Return Validation Results (recommended for API usability)

Update your PostSave method to check for validation errors using ModelState.IsValid. If invalid, return a BadRequest with error details.

Here’s the revised controller:

using System.Collections.Generic;
using System.Web.Http;
using Umbraco.Web.WebApi;
using HighlyDeveloped.Core.Models;
using HighlyDeveloped.Core.Services;

namespace HighlyDeveloped.Core.Controllers { public class BookApiController : UmbracoAuthorizedApiController { private readonly BookService _service; public BookApiController(BookService service) { _service = service; }

    [HttpGet]
    public IEnumerable&lt;Book&gt; GetAll() =&gt; _service.GetAll();

    [HttpGet]
    public Book GetById(int id) =&gt; _service.GetById(id);

    [HttpPost]
    public IHttpActionResult PostSave([FromBody] Book book)
    {
        // 1. ModelState is automatically populated based on DataAnnotations
        if (!ModelState.IsValid)
        {
            // Return all validation errors to the caller
            return BadRequest(ModelState);
        }

        if (book.Id == 0)
            _service.Add(book);
        else
            _service.Update(book);

        return Ok(book);
    }

    [HttpPost]
    public IHttpActionResult PostDelete([FromBody] int id)
    {
        _service.Delete(id);
        return Ok();
    }
}

}


3. How This Works

  • If you send invalid JSON (e.g., missing required fields, wrong format), the ModelState will have errors.

  • The API will return a 400 Bad Request with a list of validation messages.

Sample Error Response

{
  "Message": "The request is invalid.",
  "ModelState": {
    "book.Title": ["The Title field is required."],
    "book.ISBN": ["ISBN must be in the format 123-1234567890"]
  }
}

4. Tips

  • Make sure your API calls use Content-Type: application/json.

  • Test by sending incomplete or wrong data to verify you get validation errors.


Summary Table

Step What to do
Add DataAnnotations In your Book model
Check ModelState in Action In your [HttpPost] methods
Return IHttpActionResult For better API responses and errors

This is the standard/best-practice way to do validation with Web API and UmbracoAuthorizedApiController!

Here’s a complete sample AngularJS dashboard page that includes:

  • Client-side validation (immediate feedback)
  • API/server-side validation error display
  • Basic CRUD flow
  • Uses your controller pattern (BookManagerDashboardController as bookManager)

You can copy-paste this directly into your Umbraco custom dashboard HTML.


Full Sample: AngularJS + HTML

<div ng-controller="BookManagerDashboardController as bookManager">

    <h2>Book Manager</h2>

    <button class="btn btn-success mb-3" ng-click="bookManager.newBook()">Add New Book</button>

    <!-- Book List -->
    <table class="table table-striped" ng-if="!bookManager.editing">
        <thead>
            <tr>
                <th>Title</th>
                <th>Author</th>
                <th>Year</th>
                <th>ISBN</th>
                <th>Actions</th>
            </tr>
        </thead>
        <tbody>
            <tr ng-repeat="book in bookManager.books track by book.Id">
                <td>{{book.Title}}</td>
                <td>{{book.Author}}</td>
                <td>{{book.Year}}</td>
                <td>{{book.ISBN}}</td>
                <td>
                    <button class="btn btn-primary btn-sm" ng-click="bookManager.editBook(book)">Edit</button>
                    <button class="btn btn-danger btn-sm" ng-click="bookManager.deleteBook(book.Id)">Delete</button>
                </td>
            </tr>
        </tbody>
    </table>

    <!-- Book Edit/Create Form -->
    <form name="bookForm"
          novalidate
          ng-submit="bookManager.saveBook(bookForm)"
          ng-if="bookManager.editing"
          class="mb-4">

        <h4 ng-if="!bookManager.currentBook.Id">Add Book</h4>
        <h4 ng-if="bookManager.currentBook.Id">Edit Book</h4>

        <div class="form-group mb-2">
            <label>Title:</label>
            <input type="text"
                   name="title"
                   ng-model="bookManager.currentBook.Title"
                   ng-required="true"
                   ng-maxlength="150"
                   class="form-control"/>
            <span ng-show="bookForm.title.$touched && bookForm.title.$error.required" class="text-danger">
                Title is required.
            </span>
            <span ng-show="bookForm.title.$error.maxlength" class="text-danger">
                Title is too long (max 150).
            </span>
        </div>

        <div class="form-group mb-2">
            <label>Author:</label>
            <input type="text"
                   name="author"
                   ng-model="bookManager.currentBook.Author"
                   ng-required="true"
                   ng-maxlength="100"
                   class="form-control"/>
            <span ng-show="bookForm.author.$touched && bookForm.author.$error.required" class="text-danger">
                Author is required.
            </span>
            <span ng-show="bookForm.author.$error.maxlength" class="text-danger">
                Author is too long (max 100).
            </span>
        </div>

        <div class="form-group mb-2">
            <label>Year:</label>
            <input type="number"
                   name="year"
                   ng-model="bookManager.currentBook.Year"
                   ng-required="true"
                   min="1450"
                   max="2100"
                   class="form-control"/>
            <span ng-show="bookForm.year.$touched && bookForm.year.$error.required" class="text-danger">
                Year is required.
            </span>
            <span ng-show="bookForm.year.$error.min || bookForm.year.$error.max" class="text-danger">
                Year must be between 1450 and 2100.
            </span>
        </div>

        <div class="form-group mb-2">
            <label>ISBN:</label>
            <input type="text"
                   name="isbn"
                   ng-model="bookManager.currentBook.ISBN"
                   ng-required="true"
                   ng-pattern="/^\d{3}-\d{10}$/"
                   class="form-control"/>
            <span ng-show="bookForm.isbn.$touched && bookForm.isbn.$error.required" class="text-danger">
                ISBN is required.
            </span>
            <span ng-show="bookForm.isbn.$error.pattern" class="text-danger">
                ISBN must be in the format 123-1234567890.
            </span>
        </div>

        <!-- Display server-side (API) validation errors -->
        <div ng-if="bookManager.validationErrors && bookManager.validationErrors.length > 0" class="alert alert-danger">
            <ul>
                <li ng-repeat="error in bookManager.validationErrors track by $index">{{ error }}</li>
            </ul>
        </div>

        <button type="submit"
                class="btn btn-primary"
                ng-disabled="bookForm.$invalid">
            Save
        </button>
        <button type="button" class="btn btn-secondary" ng-click="bookManager.cancel()">Cancel</button>
    </form>

</div>

Controller Changes

You should slightly update your AngularJS controller to support the form reference:

angular.module("umbraco")
.controller("BookManagerDashboardController", function ($scope, $http) {
    var vm = this;
    vm.books = [];
    vm.editing = false;
    vm.currentBook = {};
    vm.validationErrors = null;

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

    vm.newBook = function () {
        vm.editing = true;
        vm.currentBook = {};
        vm.validationErrors = null;
    };

    vm.editBook = function (book) {
        vm.editing = true;
        vm.currentBook = angular.copy(book);
        vm.validationErrors = null;
    };

    // Accept the form as a parameter so we can touch all fields if needed
    vm.saveBook = function (form) {
        // Touch all fields if invalid (shows validation errors)
        if (form.$invalid) {
            angular.forEach(form.$error, function (fields) {
                angular.forEach(fields, function (field) {
                    field.$setTouched();
                });
            });
            return;
        }
        $http.post("/umbraco/backoffice/api/BookApi/PostSave", vm.currentBook)
        .then(function () {
            vm.editing = false;
            vm.loadBooks();
            vm.validationErrors = null;
        })
        .catch(function (err) {
            if (err.data && err.data.ModelState) {
                var errors = [];
                angular.forEach(err.data.ModelState, function (messages) {
                    messages.forEach(function (message) {
                        errors.push(message);
                    });
                });
                vm.validationErrors = errors;
            } else if (err.data && err.data.Message) {
                vm.validationErrors = [err.data.Message];
            } else {
                vm.validationErrors = ["An unknown error occurred."];
            }
        });
    };

    vm.deleteBook = function (id) {
        $http.post("/umbraco/backoffice/api/BookApi/PostDelete", id).then(function () {
            vm.loadBooks();
        });
    };

    vm.cancel = function () {
        vm.editing = false;
        vm.validationErrors = null;
    };

    vm.loadBooks();
});

How it Works

  • Client-side validation: Instant errors shown (required, max-length, pattern, etc.), and "Save" is disabled when invalid.
  • On submit: If the form is valid, data is posted to your API.
  • Server/API errors: If your API returns validation errors, they're shown as a list at the top of the form.
  • Edit/New: Same form is used for both, handled by checking bookManager.currentBook.Id.

This approach is clean, scalable, and fully covers both UX and API contract!

1️⃣ Search and Paging (Backoffice UI + API)

API Layer: Update the Controller and Service

Update the Service

First, update your BookService and BookRepository to support paging and searching.

/Repositories/BookRepository.cs

public (IEnumerable<Book> Books, int TotalCount) Search(string term, int page, int pageSize)
{
    using (var scope = _scopeProvider.CreateScope())
    {
        var where = string.IsNullOrWhiteSpace(term)
            ? ""
            : "WHERE Title LIKE @0 OR Author LIKE @0 OR ISBN LIKE @0";
        var query = $"SELECT * FROM Books {where} ORDER BY Id DESC OFFSET @1 ROWS FETCH NEXT @2 ROWS ONLY";
        var countQuery = $"SELECT COUNT(*) FROM Books {where}";

        var param = string.IsNullOrWhiteSpace(term) ? new object[] { } : new object[] { $"%{term}%" };

        var books = scope.Database.Fetch<Book>(query, param.Append((page - 1) * pageSize).Append(pageSize).ToArray());
        var count = scope.Database.ExecuteScalar<int>(countQuery, param);

        return (books, count);
    }
}

Note: You may need to adjust the SQL syntax if your Umbraco instance runs on a non-SQL Server database.

/Services/BookService.cs

public (IEnumerable<Book> Books, int TotalCount) Search(string term, int page, int pageSize)
    => _repo.Search(term, page, pageSize);

/Controllers/BookApiController.cs

[HttpGet]
public object Search(string term = "", int page = 1, int pageSize = 10)
{
    var result = _service.Search(term, page, pageSize);
    return new
    {
        books = result.Books,
        totalCount = result.TotalCount
    };
}

AngularJS UI: Add Search & Paging

/dashboard/dashboard.html (partial)

<input ng-model="vm.searchTerm" ng-change="vm.searchBooks()" placeholder="Search books..." class="umb-search" />
<table class="table">
  <!-- ... as before ... -->
</table>

<ul class="pagination">
  <li ng-class="{disabled: vm.page === 1}">
    <a href ng-click="vm.goToPage(vm.page-1)">&laquo;</a>
  </li>
  <li ng-repeat="p in [].constructor(vm.totalPages) track by $index"
      ng-class="{active: $index+1 === vm.page}">
    <a href ng-click="vm.goToPage($index+1)">{{$index+1}}</a>
  </li>
  <li ng-class="{disabled: vm.page === vm.totalPages}">
    <a href ng-click="vm.goToPage(vm.page+1)">&raquo;</a>
  </li>
</ul>

/dashboard/dashboard.controller.js (extended)

angular.module("umbraco")
.controller("BookManagerDashboardController", function($scope, $http) {
    var vm = this;
    vm.books = [];
    vm.editing = false;
    vm.currentBook = {};
    vm.searchTerm = "";
    vm.page = 1;
    vm.pageSize = 10;
    vm.totalCount = 0;
    vm.totalPages = 1;

    vm.searchBooks = function() {
        $http.get("/umbraco/backoffice/api/BookApi/Search", {
            params: {
                term: vm.searchTerm,
                page: vm.page,
                pageSize: vm.pageSize
            }
        }).then(function(res) {
            vm.books = res.data.books;
            vm.totalCount = res.data.totalCount;
            vm.totalPages = Math.ceil(vm.totalCount / vm.pageSize);
        });
    };

    vm.goToPage = function(page) {
        if (page < 1 || page > vm.totalPages) return;
        vm.page = page;
        vm.searchBooks();
    };

    // Override loadBooks to use search
    vm.loadBooks = function() {
        vm.page = 1;
        vm.searchBooks();
    };

    // Call search on start
    vm.loadBooks();

    // The rest (edit, new, save, delete, cancel) remain as before
});

2️⃣ Export Feature (CSV Download)

API Endpoint: Export as CSV

Add an endpoint to your API controller.

/Controllers/BookApiController.cs

[HttpGet]
public HttpResponseMessage ExportCsv()
{
    var books = _service.GetAll();
    var sb = new StringBuilder();
    sb.AppendLine("Id,Title,Author,Year,ISBN");
    foreach(var book in books)
        sb.AppendLine($"{book.Id},\"{book.Title}\",\"{book.Author}\",{book.Year},\"{book.ISBN}\"");

    var result = new HttpResponseMessage(HttpStatusCode.OK)
    {
        Content = new StringContent(sb.ToString(), Encoding.UTF8, "text/csv")
    };
    result.Content.Headers.ContentDisposition = new System.Net.Http.Headers.ContentDispositionHeaderValue("attachment")
    {
        FileName = "books-export.csv"
    };
    return result;
}

AngularJS: Download Button

Add to your HTML:

<button class="btn" ng-click="vm.exportBooks()">Export CSV</button>

Add to your controller:

vm.exportBooks = function() {
    var url = "/umbraco/backoffice/api/BookApi/ExportCsv";
    window.open(url, '_blank');
};

🚀 Result

  • Search: Type in the box to filter by title, author, or ISBN.
  • Paging: Use pagination to browse large book catalogs.
  • Export: Click “Export CSV” to download the full book list for Excel or reports.

1️⃣ Check User Role in the API Controller

Umbraco 8 exposes the current backoffice user via UmbracoContext.Security.CurrentUser.

Example: Only allow “admin” group to export or delete

using Umbraco.Core.Security; // Needed for CurrentUser

[HttpPost]
public IHttpActionResult PostDelete([FromBody] int id)
{
    var user = UmbracoContext.Security.CurrentUser;
    if (!user.Groups.Any(g => g.Alias == "admin"))
        return Unauthorized();

    _service.Delete(id);
    return Ok();
}

[HttpGet]
public HttpResponseMessage ExportCsv()
{
    var user = UmbracoContext.Security.CurrentUser;
    if (!user.Groups.Any(g => g.Alias == "admin"))
        return Request.CreateResponse(HttpStatusCode.Forbidden, "Not allowed.");

    // ...rest of the export code...
}
  • The default built-in group is “admin”, but you can check for any custom group alias.
  • You can also allow multiple groups by checking with .Any(g => allowedGroups.Contains(g.Alias)).

2️⃣ Show/Hide Actions in AngularJS UI

You can expose the current user’s groups via a new API endpoint.

Add to API Controller

[HttpGet]
public IEnumerable<string> GetCurrentUserGroups()
{
    var user = UmbracoContext.Security.CurrentUser;
    return user.Groups.Select(g => g.Alias);
}

In AngularJS controller

Add a property for permissions:

vm.userGroups = [];

vm.checkPermissions = function() {
    $http.get("/umbraco/backoffice/api/BookApi/GetCurrentUserGroups").then(function(res) {
        vm.userGroups = res.data;
    });
};
vm.checkPermissions();

vm.canDelete = function() {
    return vm.userGroups.indexOf('admin') !== -1;
};
vm.canExport = function() {
    return vm.userGroups.indexOf('admin') !== -1;
};

In HTML

<button class="btn" ng-if="vm.canExport()" ng-click="vm.exportBooks()">Export CSV</button>
<!-- ... -->
<button class="btn btn-link" ng-if="vm.canDelete()" ng-click="vm.deleteBook(book.id)">Delete</button>

3️⃣ (Optional) Block All Endpoint Access for Non-Admins

For full endpoint protection, you can use Umbraco’s UserAuthorizeAttribute and a custom attribute, or check at the start of every protected action as shown above.

Example: Custom Authorize Attribute

public class AdminOnlyAttribute : AuthorizeAttribute
{
    protected override bool IsAuthorized(HttpActionContext actionContext)
    {
        var user = Umbraco.Web.Composing.Current.UmbracoContext.Security.CurrentUser;
        return user != null && user.Groups.Any(g => g.Alias == "admin");
    }
}

Then decorate your controller actions:

[AdminOnly]
public IHttpActionResult PostDelete([FromBody] int id) { ... }

Summary

  • Backend: Only users in the “admin” group can export/delete books.
  • Frontend: UI only shows Export/Delete to admins.
  • You can extend this for any group/permission logic needed.

🎚️ Granular Permissions: Step-by-Step

You’ll need:

  • A configuration file (for easy mapping of which group can do what)
  • API controller logic to check permissions per action
  • AngularJS UI that asks the API what the user can do

1️⃣ Create a Permission Configuration

Let’s keep things flexible: Create /App_Plugins/BookManager/permissions.config.json in your solution.

{
  "add": ["admin", "editor"],
  "edit": ["admin", "editor"],
  "delete": ["admin"],
  "export": ["admin", "manager"]
}

You can update this file to add more roles/groups per action anytime!


2️⃣ Helper Class for Permission Checks

Create a helper to read the config file and check if the current user can perform a given action.

/Helpers/PermissionHelper.cs

using Newtonsoft.Json;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Umbraco.Core.Composing;
using Umbraco.Web;

namespace YourNamespace.Helpers
{
    public static class PermissionHelper
    {
        private static Dictionary<string, List<string>> _permissions;

        public static void LoadPermissions()
        {
            var configPath = System.Web.Hosting.HostingEnvironment.MapPath("~/App_Plugins/BookManager/permissions.config.json");
            var json = File.ReadAllText(configPath);
            _permissions = JsonConvert.DeserializeObject<Dictionary<string, List<string>>>(json);
        }

        public static bool UserHasPermission(string action, IEnumerable<string> userGroups)
        {
            if (_permissions == null) LoadPermissions();
            if (!_permissions.ContainsKey(action)) return false;
            return userGroups.Any(g => _permissions[action].Contains(g));
        }
    }
}

3️⃣ API Controller: Permission Checks Per Action

Update each endpoint to check permissions by action name:

using YourNamespace.Helpers; // Add this using

[HttpGet]
public object GetPermissions()
{
    var user = UmbracoContext.Security.CurrentUser;
    var groups = user.Groups.Select(g => g.Alias);

    return new
    {
        add = PermissionHelper.UserHasPermission("add", groups),
        edit = PermissionHelper.UserHasPermission("edit", groups),
        delete = PermissionHelper.UserHasPermission("delete", groups),
        export = PermissionHelper.UserHasPermission("export", groups)
    };
}

[HttpPost]
public IHttpActionResult PostSave(Book book)
{
    var user = UmbracoContext.Security.CurrentUser;
    var groups = user.Groups.Select(g => g.Alias);

    if (book.Id == 0 && !PermissionHelper.UserHasPermission("add", groups))
        return Unauthorized();

    if (book.Id != 0 && !PermissionHelper.UserHasPermission("edit", groups))
        return Unauthorized();

    if (book.Id == 0)
        _service.Add(book);
    else
        _service.Update(book);

    return Ok();
}

[HttpPost]
public IHttpActionResult PostDelete([FromBody] int id)
{
    var user = UmbracoContext.Security.CurrentUser;
    var groups = user.Groups.Select(g => g.Alias);

    if (!PermissionHelper.UserHasPermission("delete", groups))
        return Unauthorized();

    _service.Delete(id);
    return Ok();
}

[HttpGet]
public HttpResponseMessage ExportCsv()
{
    var user = UmbracoContext.Security.CurrentUser;
    var groups = user.Groups.Select(g => g.Alias);

    if (!PermissionHelper.UserHasPermission("export", groups))
        return Request.CreateResponse(HttpStatusCode.Forbidden, "Not allowed.");

    // ...export code as before...
}

4️⃣ AngularJS UI: Query Permissions, Adjust UI

In your AngularJS controller:

vm.permissions = { add: false, edit: false, delete: false, export: false };

vm.getPermissions = function() {
    $http.get("/umbraco/backoffice/api/BookApi/GetPermissions").then(function(res) {
        vm.permissions = res.data;
    });
};
vm.getPermissions();

vm.canAdd = function() { return vm.permissions.add; };
vm.canEdit = function() { return vm.permissions.edit; };
vm.canDelete = function() { return vm.permissions.delete; };
vm.canExport = function() { return vm.permissions.export; };

In your dashboard.html:

<button class="btn" ng-if="vm.canAdd()" ng-click="vm.newBook()">Add Book</button>
<button class="btn" ng-if="vm.canExport()" ng-click="vm.exportBooks()">Export CSV</button>

<!-- Table -->
<tr ng-repeat="book in vm.books">
  <!-- ... -->
  <td>
    <button class="btn btn-link" ng-if="vm.canEdit()" ng-click="vm.editBook(book)">Edit</button>
    <button class="btn btn-link" ng-if="vm.canDelete()" ng-click="vm.deleteBook(book.id)">Delete</button>
  </td>
</tr>

Recap

  • permissions.config.json controls allowed groups for each action
  • PermissionHelper centralizes all permission logic
  • API controller checks permission per action for every call
  • Frontend queries and hides/disables UI elements as appropriate

This pattern is clean, maintainable, and scalable. Update the config file—no code deploy needed for permission tweaks!

⚠️ **GitHub.com Fallback** ⚠️