Migration Guide

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