Neatoo implements a “check before execute” authorization philosophy that integrates seamlessly with the factory pattern. Rather than scattering authorization checks throughout your code, you define authorization classes that the generated factories call automatically. This ensures consistent, testable authorization that works identically on client and server.

The Authorization Philosophy

Traditional Approaches: Scattered and Fragile

In traditional applications, authorization often ends up scattered throughout the codebase:

// Controller - checks permission
public async Task<IActionResult> CreatePerson(PersonDto dto)
{
    if (!User.IsInRole("Admin"))
        return Forbid();

    // Create logic...
}

// Service layer - another check
public async Task<Person> UpdatePerson(int id, PersonDto dto)
{
    if (!_authService.CanUpdatePerson(id))
        throw new UnauthorizedException();

    // Update logic...
}

// UI - yet another check
@if (user.HasRole("Admin"))
{
    <button>Create New</button>
}

This approach has serious problems:

  • Duplication: Same checks in multiple places
  • Inconsistency: Different checks might use different logic
  • Forgotten checks: Easy to miss authorization in new code
  • Hard to test: Authorization is interleaved with business logic
  • Client-server mismatch: UI might show buttons the user cannot actually click

Neatoo’s Approach: Centralized and Automatic

Neatoo centralizes authorization in dedicated classes that integrate with the factory system:

// Define authorization rules once
public class PersonAuth : IPersonAuth
{
    private readonly IUser _user;

    public PersonAuth(IUser user) => _user = user;

    [AuthorizeFactory(AuthorizeFactoryOperation.Create)]
    public bool CanCreate() => _user.Role >= Role.Admin;

    [AuthorizeFactory(AuthorizeFactoryOperation.Fetch)]
    public bool CanFetch() => _user.Role >= Role.User;

    [AuthorizeFactory(AuthorizeFactoryOperation.Update)]
    public bool CanUpdate() => _user.Role >= Role.Editor;
}

// Link to entity
[Factory]
[AuthorizeFactory<IPersonAuth>]
internal partial class Person : EntityBase<Person>, IPerson
{
    // Factory methods are automatically authorized
}

The generated factory automatically:

  1. Resolves authorization class from DI
  2. Calls appropriate authorization methods before each operation
  3. Returns null or throws based on authorization result
  4. Provides CanXYZ() methods for UI binding

Defining Authorization Classes

An authorization class contains methods that determine whether operations are permitted.

Interface Definition

Define an interface with methods decorated with [AuthorizeFactory]:

public interface IPersonAuth
{
    [AuthorizeFactory(AuthorizeFactoryOperation.Create)]
    bool CanCreate();

    [AuthorizeFactory(AuthorizeFactoryOperation.Fetch)]
    bool CanFetch();

    [AuthorizeFactory(AuthorizeFactoryOperation.Update)]
    bool CanUpdate();

    [AuthorizeFactory(AuthorizeFactoryOperation.Delete)]
    bool CanDelete();
}

The [AuthorizeFactory] attribute specifies which operations trigger each method.

Implementation Class

Implement the interface with your authorization logic:

public class PersonAuth : IPersonAuth
{
    private readonly IUser _user;

    public PersonAuth(IUser user)
    {
        _user = user;
    }

    public bool CanCreate()
    {
        // Only admins can create
        return _user.Role >= Role.Admin;
    }

    public bool CanFetch()
    {
        // Users and above can read
        return _user.Role >= Role.User;
    }

    public bool CanUpdate()
    {
        // Editors and above can modify
        return _user.Role >= Role.Editor;
    }

    public bool CanDelete()
    {
        // Only admins can delete
        return _user.Role >= Role.Admin;
    }
}

Registering Authorization Services

Register your authorization implementation in DI:

// In Program.cs
builder.Services.AddScoped<IPersonAuth, PersonAuth>();

// The IUser service should be populated from your authentication system
builder.Services.AddScoped<IUser, User>();

The AuthorizeFactory Attribute on Entities

Link your authorization class to an entity using the [AuthorizeFactory<T>] attribute:

[Factory]
[AuthorizeFactory<IPersonAuth>]
internal partial class Person : EntityBase<Person>, IPerson
{
    // All factory operations are now authorized
    // via IPersonAuth methods

    [Create]
    public void Create() { /* ... */ }

    [Remote]
    [Fetch]
    public async Task<bool> Fetch([Service] IDbContext db) { /* ... */ }

    [Remote]
    [Insert]
    public async Task Insert([Service] IDbContext db) { /* ... */ }

    [Remote]
    [Update]
    public async Task Update([Service] IDbContext db) { /* ... */ }

    [Remote]
    [Delete]
    public async Task Delete([Service] IDbContext db) { /* ... */ }
}

How Authorization Connects to Factory Operations

The generated factory calls authorization methods based on the operation type:

Factory Operation Authorization Method Called
Create() CanCreate()
Fetch() CanFetch()
Save() (Insert) CanCreate()
Save() (Update) CanUpdate()
Save() (Delete) CanDelete()

If any called method returns false, the operation is denied.

Generated CanXYZ() Methods

For each factory operation, Neatoo generates a corresponding Can method:

public interface IPersonFactory
{
    // Operations
    IPerson? Create();
    Task<IPerson?> Fetch(Guid id);
    Task<IPerson?> Save(IPerson target);
    Task<Authorized<IPerson>> TrySave(IPerson target);

    // Authorization checks (generated when [Authorize<T>] is applied)
    Authorized CanCreate();
    Authorized CanFetch();
    Authorized CanInsert();
    Authorized CanUpdate();
    Authorized CanDelete();
    Authorized CanSave();  // Routes based on entity state
}

Using CanXYZ() in Code

Check authorization before attempting operations:

var factory = serviceProvider.GetRequiredService<IPersonFactory>();

// Check if user can create
if (factory.CanCreate().IsAuthorized)
{
    var person = factory.Create();
    // ...
}
else
{
    ShowMessage("You don't have permission to create persons.");
}

// Get authorization message
var canDelete = factory.CanDelete();
if (!canDelete.IsAuthorized)
{
    Console.WriteLine(canDelete.Message);  // Explains why not authorized
}

The Authorized Return Type

The Authorized struct provides authorization status and an optional message:

public readonly struct Authorized
{
    public bool IsAuthorized { get; }
    public string? Message { get; }

    // Factory methods
    public static Authorized Yes => new(true);
    public static Authorized No(string message) => new(false, message);
}

Authorization methods can return bool (simple) or Authorized (with message):

// Simple boolean
[AuthorizeFactory(AuthorizeFactoryOperation.Create)]
public bool CanCreate() => _user.IsAdmin;

// With message
[AuthorizeFactory(AuthorizeFactoryOperation.Create)]
public Authorized CanCreate()
{
    if (_user.IsAdmin)
        return Authorized.Yes;

    return Authorized.No("Only administrators can create new records.");
}

UI Authorization Patterns

Neatoo’s authorization model enables consistent UI experiences.

Showing/Hiding Based on Permissions

@inject IPersonFactory PersonFactory

@if (PersonFactory.CanCreate().IsAuthorized)
{
    <button @onclick="CreateNew">Create New Person</button>
}

@if (PersonFactory.CanDelete().IsAuthorized)
{
    <button @onclick="DeleteSelected" class="danger">Delete</button>
}

Disabling with Explanation

@{
    var canSave = PersonFactory.CanSave();
}

<button @onclick="Save"
        disabled="@(!canSave.IsAuthorized)"
        title="@(canSave.IsAuthorized ? "" : canSave.Message)">
    Save
</button>

Authorization-Aware Edit Pages

@page "/person/{Id:guid}"
@inject IPersonFactory PersonFactory

@if (_person == null)
{
    <p>Loading...</p>
}
else if (!_canEdit.IsAuthorized)
{
    <div class="alert alert-warning">
        @_canEdit.Message
    </div>
    <!-- Show read-only view -->
    <PersonReadOnlyView Person="_person" />
}
else
{
    <!-- Show editable form -->
    <PersonEditForm Person="_person" OnSave="HandleSave" />
}

@code {
    [Parameter] public Guid Id { get; set; }

    private IPerson? _person;
    private Authorized _canEdit;

    protected override async Task OnInitializedAsync()
    {
        _canEdit = PersonFactory.CanUpdate();
        _person = await PersonFactory.Fetch(Id);
    }
}

Combined Savability Check

Remember that IsSavable combines authorization with entity state:

@* Comprehensive save button *@
<button @onclick="Save"
        disabled="@(!_person.IsSavable || !PersonFactory.CanSave().IsAuthorized)">
    @if (_person.IsBusy)
    {
        <span>Validating...</span>
    }
    else if (!_person.IsModified)
    {
        <span>No Changes</span>
    }
    else if (!_person.IsValid)
    {
        <span>Fix Validation Errors</span>
    }
    else if (!PersonFactory.CanSave().IsAuthorized)
    {
        <span>Not Authorized</span>
    }
    else
    {
        <span>Save</span>
    }
</button>

The Check-Before-Execute Guarantee

Neatoo enforces authorization at the factory level, providing several guarantees:

Server-Side Enforcement

Even if a malicious client bypasses UI checks, the server enforces authorization:

// Client attempts unauthorized operation
var person = factory.Create();
await factory.Save(person);  // Server rejects if not authorized

Consistent Client and Server

The same authorization logic runs on both tiers:

// Client checks (instant feedback)
if (!factory.CanCreate().IsAuthorized)
{
    ShowError("Not authorized");
    return;
}

// Server also checks (authoritative)
// The factory.Create() call is authorized on server too

Atomic Authorization

All required authorization methods are checked before any operation begins:

// For Insert: HasAccess(), HasCreate(), and HasWrite() all checked
// If ANY fails, Insert never executes
// No partial operations with incomplete authorization

Contrast with Traditional Authorization

Traditional: Scattered Checks

// Must remember to check everywhere
public class PersonController : Controller
{
    [HttpPost]
    public async Task<IActionResult> Create(PersonDto dto)
    {
        // Authorization check #1
        if (!_authService.CanCreatePerson())
            return Forbid();

        var person = _mapper.Map<Person>(dto);
        await _repository.Add(person);
        return Ok();
    }

    [HttpPut("{id}")]
    public async Task<IActionResult> Update(int id, PersonDto dto)
    {
        // Authorization check #2 - different pattern
        if (!User.HasClaim("CanEditPerson", "true"))
            return Forbid();

        // ...
    }

    [HttpDelete("{id}")]
    public async Task<IActionResult> Delete(int id)
    {
        // Authorization check #3 - forgot this one!
        // Security vulnerability
        await _repository.Delete(id);
        return Ok();
    }
}

Neatoo: Centralized and Automatic

// Authorization defined once
[Factory]
[AuthorizeFactory<IPersonAuth>]
internal partial class Person : EntityBase<Person>, IPerson
{
    // All operations automatically authorized
    // Cannot forget - factory enforces it
}

// Usage is clean
var person = factory.Create();      // Authorized automatically
await factory.Save(person);         // Authorized automatically

Best Practices

Use Role-Based Authorization

Structure authorization around roles for maintainability:

public class PersonAuth : IPersonAuth
{
    public bool CanCreate() => _user.Role >= Role.Admin;
    public bool CanFetch() => _user.Role >= Role.User;
    public bool CanUpdate() => _user.Role >= Role.Editor;
}

Provide Meaningful Messages

Return Authorized with explanations for better UX:

public Authorized CanDelete()
{
    if (_user.Role < Role.Admin)
        return Authorized.No("Only administrators can delete records.");

    return Authorized.Yes;
}

Register Authorization Services as Scoped

Authorization services should be scoped to the request:

builder.Services.AddScoped<IPersonAuth, PersonAuth>();
builder.Services.AddScoped<IUser, User>();

Always Check CanXYZ() Before Showing UI Actions

Prevent confusing UX where buttons are visible but operations fail:

@if (Factory.CanDelete().IsAuthorized)
{
    <button @onclick="Delete">Delete</button>
}