| name | umbrella-dotnet-migrate-repo-controller-to-data-service |
| description | Migrate an existing GenericRepositoryApiController (Pattern 1, direct repository) to GenericRepositoryDataServiceApiController (Pattern 2, backing controller service). Creates the controller service, updates the controller, and rewires DI. Run umbrella-dotnet-rename-client-repository-to-service first if the client interface still uses the ...Repository naming convention. |
Migrate Repo Controller to Data Service Controller
Purpose
Upgrade an existing Pattern 1 API controller (inheriting GenericRepositoryApiController, direct repository access) to Pattern 2 (inheriting GenericRepositoryDataServiceApiController, backed by a controller service). This enables Blazor SSR pre-rendering support and moves all CRUD hooks into a dedicated controller service class.
Prerequisites:
- The API controller to migrate must already exist.
- If the client data interface still uses the
...Repository naming convention, run umbrella-dotnet-rename-client-repository-to-service first to rename it to ...Service and move it to Services\Abstractions\.
- If no client interface exists yet (API-only project), one will be created as part of this skill.
Discovery (read these before writing anything)
- Read the existing controller at
Web\<AppName>.Web.Server\Controllers\Api\Manage<Name>Controller.cs in full — capture the current base class, all generic type params, all constructor dependencies, and any lifecycle hook overrides (AfterCreateEntityAsync, AfterUpdateEntityAsync, AfterDeleteEntityAsync).
- Read 1–2 existing controller services in
Web\<AppName>.Web.Server\Services\ (fall back to the legacy Web\<AppName>.Web.Server\Services\Api\ location in older projects) to confirm the UmbrellaRepositoryDataService generic parameter order and project-specific patterns.
- Determine if a client service interface already exists: Check
Web\<AppName>.Web.Client.Data\Services\Abstractions\I<Name>Service.cs. If it exists (after running the rename skill), the server registration must use ReplaceScoped. If it does not exist, create it and use AddScoped.
- Read
Web\<AppName>.Web.Server\IServiceCollectionExtensions.cs — // Controller Services section — to confirm the current DI state for this feature.
Step 1 -- Ensure the service interface exists
If already created by umbrella-dotnet-rename-client-repository-to-service: skip this step.
If creating fresh (no client data project, or the interface was never created):
File: Web\<AppName>.Web.Client.Data\Services\Abstractions\I<Name>Service.cs (Blazor project)
or Web\<AppName>.Web.Server\Services\Abstractions\I<Name>Service.cs (API-only project)
using <AppName>.Web.Shared.Models.Api.<Feature>;
using Umbrella.Utilities.Data.Pagination;
namespace <AppName>.Web.[Client.Data|Server].Services.Abstractions;
public interface I<Name>Service : IGenericDataService<
<Name>Model,
int,
Slim<Name>Model,
PaginatedResultModel<Slim<Name>Model>,
Create<Name>Model,
Create<Name>ResultModel,
Update<Name>Model,
Update<Name>ResultModel>;
Use the existing controller's concrete model types. Pattern 2's generic controller and IGenericDataService contract require real keyed/read/update types and cannot use Pattern 1's object/NoOp* placeholders. If the old controller disabled an operation with placeholders, locate or scaffold the corresponding concrete models first, then preserve the disabled endpoint behavior through the controller service's XxxEndpointEnabled => false override. Stop for the model prerequisite when no correct concrete type exists.
Step 2 -- Create the controller service
File: Web\<AppName>.Web.Server\Services\<Name>ControllerService.cs
Concrete controller services live directly in the Services folder, alongside the Services\Abstractions\ interfaces folder — even when the project has existing controller services in the legacy Services\Api\ location.
Extract lifecycle hook logic and all endpoint-enablement and authorization-check overrides from the existing controller into this new class. If the controller had no hooks, the service body may contain only those behavioral overrides.
using <AppName>.Core.Data.Repositories.Abstractions;
using <AppName>.Core.Domain.Entities;
using <AppName>.Web.[Client.Data|Server].Services.Abstractions;
using <AppName>.Web.Shared.Models.Api.<Feature>;
using Umbrella.DataAccess.Abstractions;
using Umbrella.DataAccess.Abstractions.Options;
using Umbrella.Utilities.Data.Abstractions;
using Umbrella.Utilities.Data.Pagination;
using Umbrella.Utilities.Mapping.Abstractions;
using Umbrella.Utilities.Security.Abstractions;
using Umbrella.Utilities.Threading.Abstractions;
namespace <AppName>.Web.Server.Services;
public class <Name>ControllerService : UmbrellaRepositoryDataService<
<Name>Model,
Slim<Name>Model,
PaginatedResultModel<Slim<Name>Model>,
Create<Name>Model,
Create<Name>ResultModel,
Update<Name>Model,
Update<Name>ResultModel,
I<Name>Repository,
<Name>,
RepoOptions,
int>, I<Name>Service
{
public <Name>ControllerService(
ILogger<<Name>ControllerService> logger,
IHostEnvironment hostingEnvironment,
UmbrellaRepositoryDataServiceOptions options,
IUmbrellaMapper mapper,
IUmbrellaAuthorizationService authorizationService,
ISynchronizationManager synchronizationManager,
Lazy<IDataAccessUnitOfWork> dataAccessUnitOfWork,
Lazy<I<Name>Repository> repository,
IDataExpressionFactory dataExpressionFactory)
: base(logger, hostingEnvironment, options, mapper, authorizationService,
synchronizationManager, dataAccessUnitOfWork, repository, dataExpressionFactory)
{
}
protected override bool AuthorizationSlimReadChecksEnabled => false;
protected override bool AuthorizationCreateChecksEnabled => false;
protected override bool AuthorizationReadChecksEnabled => false;
protected override bool AuthorizationUpdateChecksEnabled => false;
protected override bool AuthorizationDeleteChecksEnabled => false;
}
Migrating controller behavior from the old controller:
If the original controller had overrides such as:
protected override async Task AfterCreateEntityAsync(<Name> entity, Create<Name>Model model, Create<Name>ResultModel result, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
Guard.IsNotNull(entity);
Guard.IsNotNull(model);
Guard.IsNotNull(result);
_ = await _fileHandler.CreateByGroupIdAndTempFileNameAsync(...);
UmbrellaVersionedUrl image = await _fileHandler.GetVersionedWebFilePathAsync(...)
?? throw new InvalidOperationException("The saved image could not be resolved.");
result.ImageUrl = image.Url;
result.ImageVersionToken = image.VersionToken;
}
Move lifecycle hooks verbatim into the controller service. Also move every AuthorizationXxxChecksEnabled and XxxEndpointEnabled override so authorization and disabled endpoints do not change during the migration. Preserve Dynamic Image URL/version-token pairing and asynchronous file lookups. Any extra dependencies (e.g. I<Name>FileHandler) that those hooks needed move to the controller service constructor (after the 9 base params):
private readonly I<Name>FileHandler _fileHandler;
public <Name>ControllerService(
I<Name>FileHandler fileHandler)
: base(...)
{
_fileHandler = fileHandler;
}
Rules:
public class (not internal).
IHostEnvironment in the constructor (not IWebHostEnvironment).
UmbrellaRepositoryDataService generic params (11 total): TModel first, then TSlimModel — opposite of the controller's order.
- Do not use
object or NoOp* — controller services always use full concrete model types. If the original controller used object/NoOp for some positions, use the real model types here (the service supports all operations regardless of which endpoints the controller exposes).
Step 3 -- Rewrite the controller
Replace the existing controller content entirely. The new controller is thin — no overrides, no extra dependencies:
using Microsoft.AspNetCore.Authorization;
using <AppName>.Web.[Client.Data|Server].Services.Abstractions;
using <AppName>.Web.Server.Infrastructure.Mvc;
using <AppName>.Web.Shared.Models.Api.<Feature>;
using <AppName>.Web.Shared.Security.Policies;
using Umbrella.Utilities.Data.Pagination;
namespace <AppName>.Web.Server.Controllers.Api;
[Authorize(<AppName>PolicyNames.<Policy>)]
public class <Name>Controller : <AppName>GenericRepositoryDataServiceApiController<
Slim<Name>Model,
PaginatedResultModel<Slim<Name>Model>,
<Name>Model,
Create<Name>Model,
Create<Name>ResultModel,
Update<Name>Model,
Update<Name>ResultModel,
int,
I<Name>Service>
{
public <Name>Controller(
ILogger<<Name>Controller> logger,
IWebHostEnvironment hostingEnvironment,
Lazy<I<Name>Service> repositoryDataService)
: base(logger, hostingEnvironment, repositoryDataService)
{
}
}
Important: The model types in the controller (positions T1–T7) must be the same full concrete types declared by the client interface and implemented by the controller service. If the original Pattern 1 controller used object or NoOp* to disable endpoints, replace those placeholders with the prerequisite concrete types and preserve the old availability through the matching XxxEndpointEnabled => false service override.
Step 4 -- Update DI
File: Web\<AppName>.Web.Server\IServiceCollectionExtensions.cs — // Controller Services section, alphabetical order.
If the client service interface already exists (created by umbrella-dotnet-rename-client-repository-to-service or previously scaffolded by umbrella-dotnet-scaffold-client-data):
_ = services.ReplaceScoped<I<Name>Service, <Name>ControllerService>();
If no client interface existed before this skill created it:
_ = services.AddScoped<I<Name>Service, <Name>ControllerService>();
If the old server DI had any explicit registration for this controller's old repository interface, remove it.
Analyzer compatibility
Before finishing, read .ai-shared\bundles\umbrella\analyzer-compatibility.md and build the affected projects with their installed analyzers enabled. Treat diagnostics introduced by the generated or changed code as defects in this workflow.
Verification
- The old
Manage<Name>Controller no longer inherits GenericRepositoryApiController — it inherits <AppName>GenericRepositoryDataServiceApiController with 9 type params.
- No lifecycle hooks, endpoint-enablement overrides, or authorization-check overrides remain on the controller — they live in
<Name>ControllerService.
<Name>ControllerService is public class, lives in Web\<AppName>.Web.Server\Services\, inherits UmbrellaRepositoryDataService with 11 type params, uses IHostEnvironment (not IWebHostEnvironment).
- Any extra dependencies (file handlers, services) from the old controller are now in the controller service constructor, after the 9 base params.
- Server DI uses
ReplaceScoped if a client service interface exists, AddScoped otherwise.
- The old controller's
IUmbrellaMapper, Lazy<I<Name>Repository>, and IUmbrellaRepositoryCoreDataService constructor dependencies are gone from the controller.