Skip to content

Report 24: Controllers & Services Comparison (v8 vs v17)

Generated: 2026-02-27 | Scope: All controllers, services, composing, helpers, extensions, ViewModels


1. Overview: Architectural Changes

The v8-to-v17 migration involved several fundamental architectural shifts:

Pattern v8 (Umbraco 8) v17 (Umbraco 17)
Controller model SurfaceController (MVC) ViewComponent (ASP.NET Core) or ApiController
DI pattern new Service() inline instantiation Constructor injection via IServiceCollection
Caching System.Runtime.Caching.MemoryCache.Default via static CacheLayer IMemoryCache injected via DI
HTTP calls WebRequest / HttpClient inline IHttpClientFactory injected
Config WebConfigurationManager.AppSettings[] IConfiguration["key"]
Composer IUserComposer with Composition IComposer with IUmbracoBuilder
View rendering return PartialView("path", model) return View("path", model) (ViewComponent)
Async Task.Factory.StartNew() + .Wait(3000ms) Native async/await
Service registration composition.Register<I, T>(Lifetime.Request) builder.Services.AddScoped<I, T>()
Content queries Umbraco.ContentSingleAtXPath("//...") ISiteSettingsService injected
Custom view pages ProgressViewPage<T> : UmbracoViewPage<T> @inject in Razor views
Logging ILogger (Umbraco v8) ILogger<T> (Microsoft.Extensions.Logging)

Key consolidation

  • 8 SurfaceControllers (Cookie, Help, Instagram, MobileTerms, Notification, Privacy, TermsConditions, Twitter) collapsed into 7 ViewComponents (Cookies, Help, LoginStatus, Notification, Privacy, TermsConditions, Twitter)
  • 6 calculator SurfaceControllers consolidated into 1 API controller (CalculatorApiController) + 1 service (CalculatorService)
  • 5 Headless/Heartcore services (Cookies, Help, Notifications, Privacy, TermsConditions) refactored into 2 services (HeadlessContentService + GlobalCookiesService + GlobalNotificationsService)
  • v8 CacheLayer static class eliminated -- all services use IMemoryCache DI

2. Side-by-Side: CookieSurfaceController vs CookiesViewComponent

v8: Progress.Core.SurfaceControllers.CookieSurfaceController

File: psCreditUnion/Progress.Core/SurfaceControllers/CookieSurfaceController.cs

public class CookieSurfaceController : SurfaceController
{
    public ActionResult CookiesActionResult()
    {
        var GlobalCookiesService = new GlobalCookiesService();  // Manual instantiation
        List<CookiesItem> model = new List<CookiesItem>() { };
        model = GlobalCookiesService.GetCookies();              // Synchronous
        return PartialView("~/Views/Partials/cookies/_main.cshtml", model);
    }
}

v17: www.ViewComponents.CookiesViewComponent

File: dbl.Progress/src/www/ViewComponents/CookiesViewComponent.cs

public class CookiesViewComponent : ViewComponent
{
    private readonly IGlobalCookiesService _globalCookiesService;

    public CookiesViewComponent(IGlobalCookiesService globalCookiesService)
    {
        _globalCookiesService = globalCookiesService;  // DI injection
    }

    public async Task<IViewComponentResult> InvokeAsync()
    {
        List<CookiesItem> model = await _globalCookiesService.GetCookies();  // Async
        return View("~/Views/Partials/cookies/_main.cshtml", model);
    }
}

Key differences

Aspect v8 v17
Base class SurfaceController ViewComponent
DI new GlobalCookiesService() Constructor injection
Return type ActionResult Task<IViewComponentResult>
Async No (sync wrapper around async calls) Native async/await
View invocation PartialView() View()
Interface None IGlobalCookiesService

3. Side-by-Side: Calculator Controllers

v8: CalculatorLargeSurfaceController (1 of 6)

File: psCreditUnion/Progress.Web/surfaceControllers/calculatorLargeSurfaceController.cs

The v8 approach had 6 separate surface controllers for different calculator UI variants: - calculatorIconSurfaceController.cs - calculatorInputSurfaceController.cs - calculatorInputVarRateSurfaceController.cs - calculatorLargeSurfaceController.cs - calculatorSmallInputSurfaceController.cs - calculatorSmallSurfaceController.cs

Each controller was ~260 lines, containing: - MemoryCache.Default caching with addItemToCacheMin() - Umbraco.ContentSingleAtXPath("//calculatorsConfiguration") content queries - BuildLoansDD() and BuildSelectedDetails() helpers - SubmitFormCalculations() and UpdateSelectedDropdown() POST actions - Direct ProgressLoanCalculator.LoanCalculatorEstimator usage

public class CalculatorLargeSurfaceController : SurfaceController
{
    [HttpPost]
    public JsonResult SubmitFormCalculations(calculatorViewModelLarge model)
    {
        string sRequiredLoanAmt = model.RequiredLoanAmt.Remove(0, 1);  // Remove currency symbol
        string sTerm = Regex.Replace(model.RequiredTerm, " months", "", RegexOptions.IgnoreCase);
        decimal loan = Convert.ToDecimal(sRequiredLoanAmt);
        int months = Convert.ToInt32(sTerm);
        string sRate = model.SelectedRate.Split('|')[0];
        double rate = Convert.ToDouble(sRate);

        LoanCalculatorEstimator lc = new LoanCalculatorEstimator();
        LoanCalculationResults results = lc.CalculateRepayments(loan, months, rate);
        return Json(results, JsonRequestBehavior.AllowGet);
    }
}

v17: CalculatorApiController (single controller for all)

File: dbl.Progress/src/www/Controllers/CalculatorApiController.cs

[ApiController]
[Route("api/calculator")]
public class CalculatorApiController : ControllerBase
{
    private readonly ILoanCalculatorEstimator _calculator;

    public CalculatorApiController(ILoanCalculatorEstimator calculator)
    {
        _calculator = calculator;  // DI injected
    }

    [HttpPost("calculate")]
    public IActionResult Calculate([FromForm] CalculatorRequest request)
    {
        if (request.RequiredLoanAmt <= 0 || request.RequiredTerm <= 0)
            return BadRequest("Invalid loan amount or term");

        double apr = 0;
        if (!string.IsNullOrEmpty(request.SelectedRate))
        {
            var parts = request.SelectedRate.Split('|');
            if (parts.Length > 0) double.TryParse(parts[0], out apr);
        }

        var results = _calculator.CalculateRepayments(
            principal: request.RequiredLoanAmt,
            numberOfMonths: request.RequiredTerm,
            annualInterestRate: apr);

        return Ok(new CalculatorResponse { /* mapped fields */ });
    }
}

Key differences

Aspect v8 v17
Controllers 6 separate SurfaceControllers 1 ApiController
Routing Umbraco surface controller routing [Route("api/calculator")] attribute routing
Input parsing Manual string parsing (currency/months) Strongly-typed CalculatorRequest DTO
Calculator new LoanCalculatorEstimator() ILoanCalculatorEstimator injected
Response Json(results, AllowGet) Ok(new CalculatorResponse { })
Config data Controller fetches from CMS via XPath CalculatorService provides config
Error handling Returns null Returns BadRequest()
UI data (dropdown/details) In controller Moved to CalculatorService + Razor views

4. Side-by-Side: GlobalCookiesService

v8: Progress.Core.Services.GlobalCookiesService

File: psCreditUnion/Progress.Core/Services/GlobalCookiesService.cs

public class GlobalCookiesService  // No interface
{
    string projectAlias = "thomas-s-quick-witted-raccoon";

    public void addItemToCache(object objectToCache, string key)
    {
        ObjectCache cache = MemoryCache.Default;  // Static cache
        cache.Add(key, objectToCache, DateTime.Now.AddDays(1));
    }

    public List<CookiesItem> GetCookies()  // Synchronous return
    {
        List<CookiesItem> myCacheCookies = CacheLayer.Get<List<CookiesItem>>("CookieItem");
        if (myCacheCookies == null)
        {
            Task<PagedContent> response = null;
            var tCookiesConditions = Task.Factory.StartNew(() => {
                response = GetCookiesAsync();
            });
            tCookiesConditions.Wait(TimeSpan.FromMilliseconds(3000));  // Blocking wait
            // ... process response ...
        }
        return myCacheCookies;
    }
}

v17: www.Services.GlobalCookiesService

File: dbl.Progress/src/www/Services/GlobalCookiesService.cs

public class GlobalCookiesService : IGlobalCookiesService  // Implements interface
{
    private readonly IMemoryCache _cache;      // DI injected
    private readonly IConfiguration _configuration;

    public GlobalCookiesService(IMemoryCache memoryCache, IConfiguration configuration)
    {
        _cache = memoryCache;
        _configuration = configuration;
        projectAlias = _configuration["ProgressSettings:ProjectAlias"]
            ?? throw new ArgumentNullException(nameof(configuration));
    }

    public async Task<List<CookiesItem>> GetCookies()  // Async return
    {
        if (_cache.TryGetValue("CookieItem", out List<CookiesItem>? myCacheCookies)
            && myCacheCookies is not null && myCacheCookies.Any())
        {
            return myCacheCookies;
        }

        PagedContent response = await GetCookiesAsync();  // Native async
        // ... process response ...
        return myCacheCookies;
    }
}

Key differences

Aspect v8 v17
Interface None IGlobalCookiesService
Caching MemoryCache.Default static IMemoryCache DI-injected
Config Hardcoded project alias IConfiguration["ProgressSettings:ProjectAlias"]
Async Task.Factory.StartNew().Wait(3000ms) Native async/await
Null safety No null checks Null-conditional operators, pattern matching

5. Complete File Mapping Tables

5.1 Surface Controllers --> ViewComponents / ApiController

v8 File v8 Path v17 File v17 Path Notes
CookieSurfaceController.cs Progress.Core/SurfaceControllers/ CookiesViewComponent.cs www/ViewComponents/ 1:1 migration
HelpSurfaceController.cs Progress.Core/SurfaceControllers/ HelpViewComponent.cs www/ViewComponents/ 1:1 migration
NotificationSurfaceController.cs Progress.Core/SurfaceControllers/ NotificationViewComponent.cs www/ViewComponents/ 2 actions merged to 1
PrivacySurfaceController.cs Progress.Core/SurfaceControllers/ PrivacyViewComponent.cs www/ViewComponents/ 2 actions merged to 1
TermsConditionsSurfaceController.cs Progress.Core/SurfaceControllers/ TermsConditionsViewComponent.cs www/ViewComponents/ 1:1 migration
TwitterSurfaceController.cs Progress.Core/SurfaceControllers/ TwitterViewComponent.cs www/ViewComponents/ 1:1 migration
InstagramSurfaceController.cs Progress.Core/SurfaceControllers/ -- -- Removed (Instagram API deprecated)
MobileTermsConditionsSurfaceController.cs Progress.Core/SurfaceControllers/ -- -- Merged into HeadlessContentService
-- -- LoginStatusViewComponent.cs www/ViewComponents/ New in v17

5.2 Calculator Controllers

v8 File v8 Path v17 File v17 Path Notes
calculatorLargeSurfaceController.cs Progress.Web/surfaceControllers/ CalculatorApiController.cs www/Controllers/ All 6 consolidated
calculatorInputSurfaceController.cs Progress.Web/surfaceControllers/ CalculatorApiController.cs www/Controllers/ Merged
calculatorInputVarRateSurfaceController.cs Progress.Web/surfaceControllers/ CalculatorApiController.cs www/Controllers/ Merged
calculatorIconSurfaceController.cs Progress.Web/surfaceControllers/ CalculatorApiController.cs www/Controllers/ Merged
calculatorSmallSurfaceController.cs Progress.Web/surfaceControllers/ CalculatorApiController.cs www/Controllers/ Merged
calculatorSmallInputSurfaceController.cs Progress.Web/surfaceControllers/ CalculatorApiController.cs www/Controllers/ Merged
-- -- CalculatorService.cs www/Services/ New - CMS config access

5.3 Services

v8 File v8 Path v17 File v17 Path Notes
ArticleService.cs Progress.Core/Services/ ArticleService.cs www/Services/ Migrated; HttpRequestBase --> HttpRequest
IArticleService.cs Progress.Core/Services/ IArticleService.cs www/Services/ Migrated
GlobalCookiesService.cs Progress.Core/Services/ GlobalCookiesService.cs www/Services/ Migrated; added IGlobalCookiesService
GlobalNotificationsService.cs Progress.Core/Services/ GlobalNotificationsService.cs www/Services/ Migrated; IHttpClientFactory replaces raw SDK
TwitterService.cs Progress.Core/Services/ TwitterService.cs www/Services/ Full rewrite; IHttpClientFactory, System.Text.Json
GlobalHelpCenter.cs Progress.Core/Services/ HeadlessContentService.cs www/Services/ Consolidated into general headless service
GlobalPrivacyService.cs Progress.Core/Services/ HeadlessContentService.cs www/Services/ Consolidated
GlobalTermsConditionsService.cs Progress.Core/Services/ HeadlessContentService.cs www/Services/ Consolidated
GlobalMobileTermsConditionsService.cs Progress.Core/Services/ HeadlessContentService.cs www/Services/ Consolidated
CommonBondValidationService.cs Progress.Core/Services/ CommonBondValidationService.cs Progress.CustomPropertyEditors/Forms/Services/ Moved to Forms project
GalleryService.cs Progress.Core/Services/ -- -- Not migrated (see Section 6)
IGalleryService.cs Progress.Core/Services/ -- -- Not migrated
InstagramService.cs Progress.Core/Services/ -- -- Removed (API deprecated)
MigrateNewsCategoriesComposer.cs Progress.Core/Services/ -- -- One-time migration, not needed
-- -- CalculatorService.cs www/Services/ New
-- -- ICalculatorService.cs www/Services/ New
-- -- DictionaryService.cs www/Services/ New - replaces Umbraco.GetDictionaryValue()
-- -- IDictionaryService.cs www/Services/ New
-- -- IHeadlessContentService.cs www/Services/ New
-- -- ITwitterService.cs www/Services/ New
-- -- IGlobalCookiesService.cs www/Services/ New
-- -- IGlobalNotificationsService.cs www/Services/ New
-- -- CacheCleanupService.cs www/Services/ New - pre-startup cache cleaning
-- -- DatabaseCleanupService.cs www/Services/ New - pre-startup DB cleaning

5.4 Composing / Composers

v8 File v8 Path v17 File v17 Path Notes
RegisterServicesComposer.cs Progress.Core/Composing/ ServiceComposer.cs www/Composing/ Migrated; registers Twitter, Cookies
CommonBondValidationComposer.cs Progress.Core/Composing/ CommonBondValidationHandler.cs CustomPropertyEditors/Forms/Validation/ Refactored to notification handler
LostYourPinComposer.cs Progress.Core/Composing/ LostYourPinValidationHandler.cs + LostYourPinPrePopulateHandler.cs CustomPropertyEditors/Forms/Validation/ Split into separate handlers
MortgageCalculationComposer.cs Progress.Core/Composing/ MortgageCalculationHandler.cs CustomPropertyEditors/Forms/Workflows/ Moved to forms project
ReCaptchaFallbackComposer.cs Progress.Core/Composing/ ReCaptchaFallbackHandler.cs + FriendlyCaptchaValidationHandler.cs CustomPropertyEditors/Forms/Validation/ Split
RemoveDashBoard.cs Progress.Core/Composing/ RemoveDashboards/ www/App_Plugins/RemoveDashboards/ Client-side approach in v17
SubscribeToContentServiceSavingComposer.cs Progress.Core/Composing/ -- -- ImageProcessor/WebP removed (v17 uses ImageSharp). Content saving permissions TBD
TreeNodeRenderingComposer.cs Progress.Core/Composing/ -- -- Not needed (v17 backoffice handles icons natively)
UserServiceComposer.cs Progress.Core/Composing/ -- -- Not migrated yet (user group protection)
-- -- McpApiUserComposer.cs www/Composing/ New - creates MCP API user on startup
ExcludeItems.cs Progress.Web/umbracoFormsHideField/ UrlValidationNotificationHandler.cs CustomPropertyEditors/Forms/Validation/ URL validation extracted

5.5 Extensions

v8 File v8 Path v17 File v17 Path Notes
HtmlExtensions.cs Progress.Core/Extensions/ HtmlExtensions.cs www/Extensions/ GetGridHtml removed (no Grid); Breadcrumb commented out
EnumerableExtensions.cs Progress.Core/Extensions/ EnumerableExtensions.cs www/Extensions/ Direct port; List<T> --> collection expression []
IPublishedContentExtensions.cs Progress.Core/Extensions/ -- -- IsHomePage() inlined where needed
HttpRequestBaseExtensions.cs Progress.Core/Extensions/ -- -- HttpRequestBase does not exist in ASP.NET Core
-- -- BlockGridExtensions.cs www/Extensions/ New - replaces v8 Grid rendering helpers

5.6 Helpers

v8 File v8 Path v17 File v17 Path Notes
CacheLayer.cs Progress.Core/Helpers/ -- -- Removed - replaced by IMemoryCache DI
HtmlHelpers.cs Progress.Core/Helpers/ -- -- Functionality moved to extensions/views
QueryStringHelper.cs Progress.Core/Helpers/ -- -- Inlined into ArticleService as GetIntFromQueryString()
ResponsiveHelper.cs Progress.Core/Helpers/ -- -- ImageProcessor-based; replaced by ImageSharp in v17
TagAttributes.cs Progress.Core/Helpers/ -- -- Used by ResponsiveHelper only
-- -- CssHelper.cs www/Helpers/ New - replaces ClientDependency RequiresCss()
-- -- ScriptHelper.cs www/Helpers/ New - replaces ClientDependency RequiresJs()

5.7 ViewModels

v8 File v8 Path v17 File v17 Path Notes
CookiesList.cs Progress.Core/ViewModels/ CookiesList.cs www/ViewModels/ Migrated
notificationList.cs Progress.Core/ViewModels/ NotificationItem.cs www/ViewModels/ Renamed; PascalCase
ArticleResultSet.cs Progress.Core/ViewModels/ ArticleResultSet.cs www/ViewModels/ Migrated
TweetSearchResponse.cs Progress.Core/ViewModels/ TweetSearchResponse.cs www/ViewModels/ Migrated; added errorMessage field
HelpList.cs Progress.Core/ViewModels/ -- -- Merged into HeadlessContentService models
HelpArticleIdList.cs Progress.Core/ViewModels/ -- -- Merged
PrivacyList.cs Progress.Core/ViewModels/ -- -- Merged into HeadlessContent.cs
TermsConditionsList.cs Progress.Core/ViewModels/ -- -- Merged
MobileTermsConditionsList.cs Progress.Core/ViewModels/ -- -- Merged
GalleryResultSet.cs Progress.Core/ViewModels/ -- -- Not migrated (GalleryService not migrated)
Instagram.cs Progress.Core/ViewModels/ -- -- Removed
-- -- HeadlessContent.cs www/ViewModels/ New - consolidates Help/Privacy/T&C models

Calculator ViewModels:

v8 File v8 Path v17 File v17 Path Notes
calculatorViewModelLarge.cs Progress.Web/ViewModels/ CalculatorRequest + CalculatorResponse www/Controllers/ (inline) Inlined into API controller
calculatorViewModelInput.cs Progress.Web/ViewModels/ -- -- Merged into single request model
calculatorViewModelInputVarRate.cs Progress.Web/ViewModels/ -- -- Merged
calculatorViewModelIcon.cs Progress.Web/ViewModels/ -- -- Merged
calculatorViewModelSmall.cs Progress.Web/ViewModels/ -- -- Merged

5.8 ViewPages

v8 File v8 Path v17 File v17 Path Notes
ProgressViewPage.cs Progress.Core/ViewPages/ -- -- Removed - v17 uses @inject instead
ProgressGalleryViewPage.cs Progress.Core/ViewPages/ -- -- Removed

5.9 PropertyValueConverters

v8 File v8 Path v17 File v17 Path Notes
-- -- OpeningSoonValueConverter.cs www/PropertyValueConverters/ New - converts OpeningSoon JSON to IEnumerable<OpeningHoursDay>

5.10 Umbraco Forms

v8 File v8 Path v17 File v17 Path Notes
DropDownYears.cs Progress.Web/UmbracoFormsExtension/ DropdownListWithYears.cs CustomPropertyEditors/Forms/FieldTypes/ Migrated
FriendlyCaptcha.cs Progress.Web/UmbracoFormsExtension/ FriendlyCaptchaField.cs CustomPropertyEditors/Forms/FieldTypes/ Migrated
LostYourPinField.cs Progress.Web/UmbracoFormsExtension/ LostYourPinField.cs CustomPropertyEditors/Forms/FieldTypes/ Migrated
MortgageCalculatorResult.cs Progress.Web/UmbracoFormsExtension/ MortgageCalculatorResult.cs CustomPropertyEditors/Forms/FieldTypes/ Migrated
TextareaWithCount.cs Progress.Web/UmbracoFormsExtension/ TextareaWithCount.cs CustomPropertyEditors/Forms/FieldTypes/ Migrated
TextfieldWithCount.cs Progress.Web/UmbracoFormsExtension/ TextfieldWithCount.cs CustomPropertyEditors/Forms/FieldTypes/ Migrated
CancelUploadedFiles.cs Progress.Web/UmbracoFormsWorkFlows/ CancelUploadedFilesWorkflow.cs CustomPropertyEditors/Forms/Workflows/ Migrated
PostAsJson.cs Progress.Web/UmbracoFormsWorkFlows/ PostAsJsonWorkflow.cs CustomPropertyEditors/Forms/Workflows/ Migrated

New Forms files in v17 (no v8 equivalent):

v17 File v17 Path Notes
CommonBondMigrationPlan.cs Forms/Migrations/ EF Core migration for CommonBond table
CreateCommonBondTableMigration.cs Forms/Migrations/ Table creation migration
CommonBondRange.cs Forms/Models/ EF Core model
LostYourPinModels.cs Forms/Models/ API request/response models
ICommonBondValidationService.cs Forms/Services/ Interface (v8 had no interface)
ILostYourPinService.cs Forms/Services/ Interface
LostYourPinService.cs Forms/Services/ Extracted from composer
CommonBondValidationHandler.cs Forms/Validation/ Notification handler
FriendlyCaptchaValidationHandler.cs Forms/Validation/ Notification handler
LostYourPinPrePopulateHandler.cs Forms/Validation/ Notification handler
LostYourPinValidationHandler.cs Forms/Validation/ Notification handler
ReCaptchaFallbackHandler.cs Forms/Validation/ Notification handler
UrlValidationNotificationHandler.cs Forms/Validation/ Notification handler
MortgageCalculationHandler.cs Forms/Workflows/ Workflow handler

6. Gaps: Why Some Services Were Not Migrated

GalleryService + IGalleryService

v8 files: Progress.Core/Services/GalleryService.cs, Progress.Core/Services/IGalleryService.cs

The GalleryService provides paginated gallery content using IPublishedContent.Descendants(). It is structurally identical to ArticleService but for gallery content types. It was not migrated because: 1. The gallery feature is not actively used by Progress Credit Union 2. The galleryItems content type may not exist in the v17 destination database 3. If needed, it can be added by copying the ArticleService pattern and changing "article" to "galleryItems"

InstagramService + InstagramSurfaceController

v8 files: Progress.Core/Services/InstagramService.cs, Progress.Core/SurfaceControllers/InstagramSurfaceController.cs

Removed because: 1. Instagram deprecated the /?__a=1 endpoint used by the v8 service 2. The OAuth code contained hardcoded client credentials from a specific Instagram app 3. Instagram now requires Facebook Graph API with a Business account 4. The feature was rarely used and can be replaced with an embedded Instagram widget if needed

MigrateNewsCategoriesComposer

v8 file: Progress.Core/Services/MigrateNewsCategoriesComposer.cs

This was a one-time data migration composer that ran on v8 startup to restructure news categories. It is not needed in v17 because the migration tool handles data transformation.

SubscribeToContentServiceSavingComposer (partial)

v8 file: Progress.Core/Composing/SubscribeToContentServiceSavingComposer.cs

The ImageProcessor WebP conversion logic is no longer needed (v17 uses ImageSharp). The unpublish permission check and calculator cache invalidation on content save have not been migrated yet. The calculator cache invalidation is partially handled by CalculatorService's time-based cache expiry.

UserServiceComposer

v8 file: Progress.Core/Composing/UserServiceComposer.cs

Protects the creditUnionAdministrators and creditUnionEditor user groups from modification/deletion by non-admin users. This has not been migrated to v17 yet. In v17, this would use INotificationHandler<UserGroupSavingNotification> and INotificationHandler<UserGroupDeletingNotification>.

TreeNodeRenderingComposer

v8 file: Progress.Core/Composing/TreeNodeRenderingComposer.cs

Updates backoffice tree node icons to match content type icons. Not needed in v17 because the new Bellissima backoffice handles this natively.


7. Summary Statistics

Category v8 Count v17 Count Migrated New Removed/Deferred
Surface Controllers 14 (8 core + 6 calc) 0 -- -- Superseded
ViewComponents 0 7 -- 7 --
API Controllers 0 1 -- 1 --
Services 14 16 7 9 3 removed, 1 deferred
Composing 9 + 1 ExcludeItems 2 1 1 8 refactored to Forms handlers
Extensions 4 3 2 1 2 removed
Helpers 5 2 0 2 5 removed
ViewModels 16 (11 core + 5 web) 5 4 1 11 removed/merged
ViewPages 2 0 0 0 2 removed
PropertyValueConverters 0 1 0 1 --
Forms FieldTypes 6 6 6 0 --
Forms Workflows 2 3 2 1 --
Forms Validation 0 (in composers) 6 -- 6 Extracted from composers
Forms Services 0 (inline) 4 -- 4 Extracted
Forms Models 0 (inline) 2 -- 2 Extracted
Forms Migrations 0 2 -- 2 New EF Core
Total ~68 ~60 ~22 ~37 new ~18 removed
Migration documentation by Double for Progress Credit Union