Agility CMS documentationAgility CMS documentation
OverviewEditorsDevelopersOwners & AdminsTraining GuideApps
Sign inLet's Chat
.NET
ConfigurationDeploy to AzureFetching ContentMigration Guide

Tutorials

Migration Guide

This guide helps you migrate from older Agility CMS .NET implementations to the modern Blazor or MVC starters.

This guide helps you migrate from older Agility CMS .NET implementations to the modern Blazor or MVC starters.

Overview of Changes

The new .NET starters represent a significant modernization of Agility CMS integration:

AspectLegacy (.NET MVC 4/5)Modern (.NET 8+)
Content AccessContent RepositoryFetch API
Page Managementglobal.asax routesMiddleware + DI
URL RedirectsBuilt-in, requiredOpt-in middleware
CachingAutomaticConfigurable
Content TypesManual mappingTyped endpoints + CLI
APIREST onlyREST + GraphQL

Migration Path

Step 1: Assess Your Current Implementation

Identify what you're using from the legacy SDK:

  1. Content Repository - Items(), GetById(), GetItemsByIDs()
  2. Page Management - Routes in global.asax
  3. URL Redirects - Built-in redirect handling
  4. Content Models - Manual class definitions

Step 2: Set Up the New Project

  1. Clone the new starter (Blazor or MVC)
  2. Configure your API keys
  3. Run locally to verify connection
git clone https://github.com/agility/agilitycms-dotnet-starter.git
cd agilitycms-dotnet-starter/Agility.NET.Blazor.Starter
# Add appsettings.local.json with your keys
dotnet watch

Step 3: Migrate Components

Your biggest task is converting views/components. The content models and logic remain similar.

Migrating Content Access

Before: Content Repository

// Legacy approach
public class BlogController : Controller
{
    public ActionResult Index()
    {
        var repository = new ContentRepository();
        var posts = repository.Items<Post>("posts")
            .Where(p => p.Status == "published")
            .OrderByDescending(p => p.Date)
            .Take(10)
            .ToList();

        return View(posts);
    }

    public ActionResult Detail(int id)
    {
        var repository = new ContentRepository();
        var post = repository.GetById<Post>(id);
        return View(post);
    }
}

After: Fetch API Service

// Modern approach - Blazor
@inject FetchApiService FetchApi

@code {
    private List<Post>? posts;

    protected override async Task OnInitializedAsync()
    {
        posts = await FetchApi.GetTypedContentList<Post>(
            referenceName: "posts",
            locale: "en-us",
            take: 10,
            sort: "fields.date",
            direction: "desc"
        );
    }
}
// Modern approach - MVC
public class BlogController : Controller
{
    private readonly FetchApiService _fetchApi;

    public BlogController(FetchApiService fetchApi)
    {
        _fetchApi = fetchApi;
    }

    public async Task<IActionResult> Index()
    {
        var posts = await _fetchApi.GetTypedContentList<Post>(
            referenceName: "posts",
            locale: "en-us",
            take: 10,
            sort: "fields.date",
            direction: "desc"
        );

        return View(posts);
    }
}

Key Differences

LegacyModern
repository.Items<T>("reference")fetchApi.GetTypedContentList<T>("reference", locale)
repository.GetById<T>(id)fetchApi.GetTypedContentItem<T>(id, locale)
LINQ filteringAPI filter parameter
SynchronousAsync/await

Migrating Page Management

Before: Global.asax Routes

// Global.asax.cs
protected void Application_Start()
{
    // Agility route registration
    AgilityRouteConfig.RegisterRoutes(RouteTable.Routes);
}

After: Middleware + Dependency Injection

// Program.cs
var builder = WebApplication.CreateBuilder(args);

// Register services
builder.Services.AddSingleton<FetchApiService>(...);
builder.Services.AddSingleton<AgilityRouteTransformer>();

var app = builder.Build();

// Configure middleware
app.UseStaticFiles();
app.UseRouting();
app.UseAgilityRedirects();  // Optional: URL redirects

// Dynamic page routing
app.MapDynamicPageRoute<AgilityRouteTransformer>("{**slug}");

Key Differences

  • Routes are now opt-in via dependency injection
  • Page routing uses ASP.NET Core's DynamicRouteValueTransformer
  • URL redirects are a separate middleware you can customize or omit

Migrating URL Redirects

Before: Automatic & Required

URL redirects were always enabled with no customization.

After: Opt-In Middleware

// Program.cs - Add only if you need redirects
app.UseAgilityRedirects();

Or create custom redirect logic:

// Custom redirect middleware
public class CustomRedirectMiddleware
{
    private readonly RequestDelegate _next;
    private readonly FetchApiService _fetchApi;

    public async Task InvokeAsync(HttpContext context)
    {
        var redirects = await _fetchApi.GetUrlRedirects("en-us");
        var match = redirects.FirstOrDefault(r =>
            r.OriginUrl.Equals(context.Request.Path, StringComparison.OrdinalIgnoreCase));

        if (match != null)
        {
            // Custom logic here
            context.Response.Redirect(match.DestinationUrl, permanent: true);
            return;
        }

        await _next(context);
    }
}

Migrating Content Models

Before: Manual Class Definitions

// Models/Post.cs
public class Post
{
    public int ContentID { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }
    public DateTime Date { get; set; }
    public string Slug { get; set; }
}

After: Typed Models (Same, but Better Organized)

// Models/AgilityModels.cs
public class Post
{
    public string? Title { get; set; }
    public string? Slug { get; set; }
    public DateTime? Date { get; set; }
    public string? Excerpt { get; set; }
    public string? Content { get; set; }
    public ImageAttachment? Image { get; set; }
    public ContentReference? Category { get; set; }
}

public class ImageAttachment
{
    public string? Url { get; set; }
    public string? Label { get; set; }
    public int? Width { get; set; }
    public int? Height { get; set; }
}

Using the Agility CLI (Optional)

Generate models from your Agility content definitions:

# Install the CLI
dotnet tool install -g agility-cli

# Generate models
agility models generate --output ./Models

Migrating Views

Before: MVC 4/5 Views

@* Views/Blog/Index.cshtml *@
@model IEnumerable<Post>

<div class="posts">
    @foreach (var post in Model)
    {
        <article>
            <h2><a href="/blog/@post.Slug">@post.Title</a></h2>
            <time>@post.Date.ToString("MMMM d, yyyy")</time>
            @Html.Raw(post.Excerpt)
        </article>
    }
</div>

After: Blazor Component

@* Components/AgilityComponents/PostsListing.razor *@

<div class="posts grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
    @if (posts != null)
    {
        @foreach (var post in posts)
        {
            <article class="bg-white rounded-lg shadow p-6">
                <h2 class="text-xl font-bold mb-2">
                    <a href="/blog/@post.Slug" class="hover:text-primary-500">
                        @post.Title
                    </a>
                </h2>
                <time class="text-gray-500 text-sm">
                    @post.Date?.ToString("MMMM d, yyyy")
                </time>
                <div class="mt-4 prose">
                    @((MarkupString)(post.Excerpt ?? ""))
                </div>
            </article>
        }
    }
</div>

@code {
    private List<Post>? posts;

    protected override async Task OnInitializedAsync()
    {
        posts = await AgilityService.GetPostsAsync("en-us");
    }
}

After: MVC ViewComponent

// ViewComponents/PageModules/PostsListing.cs
public class PostsListing : ViewComponent
{
    private readonly FetchApiService _fetchApi;

    public PostsListing(FetchApiService fetchApi)
    {
        _fetchApi = fetchApi;
    }

    public async Task<IViewComponentResult> InvokeAsync(ModuleModel moduleModel)
    {
        var posts = await _fetchApi.GetTypedContentList<Post>(
            "posts", moduleModel.Locale, take: 10);

        return View("/Views/PageModules/PostsListing.cshtml", posts);
    }
}
@* Views/PageModules/PostsListing.cshtml *@
@model List<Post>

<div class="posts grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
    @foreach (var post in Model)
    {
        <article class="bg-white rounded-lg shadow p-6">
            <h2 class="text-xl font-bold mb-2">
                <a href="/blog/@post.Slug">@post.Title</a>
            </h2>
            <time class="text-gray-500 text-sm">
                @post.Date?.ToString("MMMM d, yyyy")
            </time>
            <div class="mt-4 prose">
                @Html.Raw(post.Excerpt)
            </div>
        </article>
    }
</div>

API Method Mapping

Legacy MethodModern Method
repository.Items<T>(ref)GetTypedContentList<T>(ref, locale)
repository.GetById<T>(id)GetTypedContentItem<T>(id, locale)
repository.GetItemsByIDs<T>(ids)Loop with GetTypedContentItem<T> or GraphQL
N/AGetContentByGraphQL<T>(query, objName, locale)
N/AGetTypedPage(pageId, locale)
N/AGetSitemapFlat(channel, locale)
N/AGetUrlRedirects(locale)

Configuration Migration

Before: Web.config

<appSettings>
    <add key="Agility.ContentAccessor.InstanceGuid" value="..." />
    <add key="Agility.ContentAccessor.ApiKey" value="..." />
</appSettings>

After: appsettings.json

{
  "AppSettings": {
    "InstanceGUID": "your-guid",
    "SecurityKey": "your-key",
    "FetchAPIKey": "defaultlive.your-key",
    "PreviewAPIKey": "defaultpreview.your-key",
    "Locales": "en-us",
    "ChannelName": "website",
    "CacheInMinutes": 5
  }
}

Feature Comparison

What's New

  • GraphQL Support - Query exactly the fields you need
  • Typed Endpoints - GetTypedContentList<T>, GetTypedPage, etc.
  • Preview Mode - Built-in preview with security key validation
  • CDN Headers - Stale-while-revalidate for better caching
  • Webhook Support - Cache invalidation on publish
  • Tailwind CSS - Modern utility-first styling
  • One-Click Deploy - ARM templates for Azure

What's Different

  • Async by Default - All API methods are async
  • Dependency Injection - Services injected, not instantiated
  • Explicit Locale - Pass locale to every API call
  • Opt-In Features - Page routing and redirects are optional

What's Removed

  • Synchronous API - Use async/await everywhere
  • Built-In Caching - Implement your own caching strategy
  • Global.asax - Use middleware pipeline

Migration Checklist

  • Set up new project (Blazor or MVC starter)
  • Configure API keys in appsettings.local.json
  • Define content models in Models/AgilityModels.cs
  • Convert controllers to ViewComponents or Blazor components
  • Update views to use new syntax
  • Migrate any custom routing logic
  • Set up caching if needed
  • Configure webhooks for cache invalidation
  • Test preview mode
  • Deploy to Azure
  • Configure production webhooks and preview URLs

Getting Help

If you encounter issues during migration:

  1. Check the GitHub Issues
  2. Join the Agility CMS Community
  3. Contact Agility Support

Next Steps

  • Getting Started - Set up the new project
  • Blazor Starter - Blazor-specific features
  • MVC Starter - MVC-specific features
  • Fetching Content - New API methods
  • Configuration - Configuration options
← Previous
Fetching Content
On this page
Overview of ChangesMigration PathMigrating Content AccessMigrating Page ManagementMigrating URL RedirectsMigrating Content ModelsMigrating ViewsAPI Method MappingConfiguration MigrationFeature ComparisonMigration ChecklistGetting HelpNext Steps
Agility CMS documentationAgility CMS documentation

Documentation for the CMS built for editors, developers, and AI agents.

Docs
  • Overview
  • Editors
  • Developers
  • Owners & Admins
  • Training Guide
  • Changelog
Resources
  • Get Support
  • MCP Server
  • System Status
  • llms.txt
Agility
  • agilitycms.com
  • Start Free Trial
  • Sign in
  • Blog
© 2026 Agility Inc. All rights reserved.
Privacy PolicyTerms of Service