Umbraco 16 API Controller Registration Issue

Problem Summary

I’m trying to create custom API controllers in Umbraco 16 but they’re not being discovered/registered by the routing system, returning 404 errors. Even when following the new ASP.NET Core patterns, the controllers aren’t accessible.

Working Environment

  • Umbraco Version: 16.0.0
  • Target Framework: .NET 9.0
  • Project Type: Paul Seal’s Clean Starter Kit for Umbraco 16
  • Development Environment: macOS, running on localhost:10311

What Works

:white_check_mark: Existing API controllers work perfectly:

:white_check_mark: Service registration works:

  • Custom services registered via Composer pattern are working
  • Dependency injection functioning correctly
  • Umbraco dashboard loads and displays properly

What Doesn’t Work

:cross_mark: New API controllers return 404:

  • Custom controllers not being discovered
  • Both old UmbracoApiController pattern and new ASP.NET Core patterns fail
  • Adding endpoints to existing working controllers also fails

Attempts Made

  1. Traditional UmbracoApiController Pattern (Failed)
  [Route("umbraco/api/[controller]/[action]")]
  public class TestApiController : UmbracoApiController
  {
      [HttpGet]
      public IActionResult Status() => Ok(new { success = true });
  }

Result: 404 on /umbraco/api/TestApi/Status

  1. Versioned API Pattern (Failed)
  [Route("api/v{version:apiVersion}/ai")]
  [ApiVersion("1.0")]
  [MapToApi("umbraco16cogaiassistant-starter")]
  [ApiExplorerSettings(GroupName = "AI Assistant")]
  [ApiController]
  public class AIAssistantApiV1Controller : UmbracoApiController
  {
      [HttpGet("ping")]
      public IActionResult Ping() => Ok(new { success = true });
  }

Result: 404 on /api/v1.0/ai/ping

  1. Standard ASP.NET Core Pattern (Failed)
  [ApiController]
  [Route("/api/ai")]
  public class AIApiController : Controller
  {
      [HttpPost("test")]
      public IActionResult Test() => Ok(new { success = true });
  }

Result: 404 on /api/ai/test

  1. Adding to Existing Working Controller (Failed)

Added new endpoints to the working DictionaryApiV1Controller:

 [HttpGet]
  [Route("testai")]
  public IActionResult TestAI() => Ok(new { success = true });

Result: 404 on /api/v1.0/dictionary/testai (even though /api/v1.0/dictionary/getdictionarytranslations works)

Current Program.cs Configuration

  WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

  // Add controllers for AI API
  builder.Services.AddControllers();

  builder.CreateUmbracoBuilder()
      .AddBackOffice()
      .AddWebsite()
      .AddComposers()
      .Build();

  WebApplication app = builder.Build();

  await app.BootUmbracoAsync();

  app.UseUmbraco()
      .WithMiddleware(u =>
      {
          u.UseBackOffice();
          u.UseWebsite();
      })
      .WithEndpoints(u =>
      {
          u.UseBackOfficeEndpoints();
          u.UseWebsiteEndpoints();
      });

  // Map controllers AFTER Umbraco configuration
  app.MapControllers();

  await app.RunAsync();

Project Structure

  • Main Project: Umbraco16CogAIAssistant.Blog (Web project)
  • Custom Services: UmbracoAI.Assistant (Class library with controllers)
  • Working APIs: Umbraco16CogAIAssistant.Headless (Contains working dictionary API)

The UmbracoAI.Assistant project is properly referenced in the main project.

Specific Questions

  1. Why do existing controllers work but new ones don’t? Even adding endpoints to working controllers fails.
  2. Is there a specific registration pattern for Umbraco 16? The documentation mentions moving away from
    UmbracoApiController but examples are limited.
  3. Controller discovery issue? Do controllers need to be in specific projects/namespaces?
  4. Middleware order? Tried MapControllers() both before and after UseUmbraco().
  5. Missing configuration? Is there additional setup needed for custom API controllers in Umbraco 16?

What I’ve Tried

  • Different base classes (UmbracoApiController, Controller, ManagementApiControllerBase)
  • Various routing patterns and attributes
  • Multiple MapControllers() positions in middleware pipeline
  • Adding controllers to different projects in the solution
  • Verifying service registration and dependency injection
  • Restarting application between changes

Expected Behavior

Custom API controllers should be accessible and return JSON responses, similar to how the existing dictionary API works.

Additional Context

This is for an AI Assistant integration that needs HTTP endpoints for a dashboard interface. The core functionality (OpenAI integration, services, dashboard UI) all work perfectly - only the HTTP API routing is failing.

Any insights into Umbraco 16’s API controller registration mechanism would be greatly appreciated!

Here is an example search controller that works with Umbraco 16. It accepts a POST request and returns JSON. You can change it to GET if desired. Url is /MySiteSearch/Search/

using My.Core.Models;
using My.Core.SiteSearch.Models;
using My.Core.SiteSearch.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Web;
using Umbraco.Cms.Infrastructure.Persistence;

namespace My.Core.SiteSearch.Controllers
{
	[ApiController]
	[Route("[controller]/[action]")]
	public class MySiteSearchController : ControllerBase
    {
        private readonly ILogger<MySiteSearchController> _logger;
        private IMySiteSearchService _mySiteSearchService { get; set; }
        public MySiteSearchController(IUmbracoContextAccessor umbracoContextAccessor,
            IUmbracoDatabaseFactory databaseFactory,
            ServiceContext services,
            AppCaches appCaches,
            ILogger<MySiteSearchController> logger,
            IPublishedUrlProvider publishedUrlProvider,
            IMySiteSearchService mySiteSearchService)
            
        {
            _mySiteSearchService = mySiteSearchService;
            _logger = logger;
        }


        // /MySiteSearch/Search/
        [HttpPost]
        public ApiResponseMessage<SearchResults> Search([FromBody] SearchCriteria criteria)
        {
            ApiResponseMessage<SearchResults> results = new ApiResponseMessage<SearchResults>();
            results.Messages = new List<string>();

            try
            {
                results.Data = _mySiteSearchService.Search(new SearchCriteria()
                {
                    IndexId = criteria.IndexId,
                    PageNumber = criteria.PageNumber,
                    Term = criteria.Term
                });
                results.Success = true;
            }
            catch (Exception ex)
            {
                results.Messages.Add(ex.Message);
                _logger.LogError($"Error in search request: {ex.Message}", new { exception = ex });
            }
            return results;
        }
    }
}

1 Like

Heya Ad
I would play a game of spot the difference when using the Umbraco dotnet new templates and use the dotnet new umbraco-extension and see if you can see how/why that differs to your setup.

Curious to know what the problem is for you.

1 Like

Just to be sure, you checked this doc right?

And this dashboard you mention is not a dashboard in Umbraco, right?

1 Like

Thanks for your help everyone.

I’m not 100% sure how I fixed it but it works now :slight_smile:

I think it it was something to do with the following not being configured properly. Although I did have to start from fresh as I lost everything I started with due to a pretty fundamemntal mistake with not backing up the original solution in GitHub :upside_down_face:

Authentication Pattern

import { UMB_AUTH_CONTEXT } from '@umbraco-cms/backoffice/auth';

export class UmbracoAIDashboardElement extends UmbElementMixin(LitElement) {
  constructor() {
    super();
    // Consume auth context for Management API access
    this.consumeContext(UMB_AUTH_CONTEXT, (authContext) => {
      this._authContext = authContext;
    });
  }

  async _getAuthHeaders() {
    const umbOpenApi = this._authContext.getOpenApiConfiguration();
    const token = typeof umbOpenApi.token === 'function' 
      ? await umbOpenApi.token() 
      : umbOpenApi.token;
    
    return {
      'Authorization': `Bearer ${token}`
    };
  }
}