See Agility CMS in action. Watch a product demo
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:
| Aspect | Legacy (.NET MVC 4/5) | Modern (.NET 8+) |
|---|---|---|
| Content Access | Content Repository | Fetch API |
| Page Management | global.asax routes | Middleware + DI |
| URL Redirects | Built-in, required | Opt-in middleware |
| Caching | Automatic | Configurable |
| Content Types | Manual mapping | Typed endpoints + CLI |
| API | REST only | REST + GraphQL |
Migration Path
Step 1: Assess Your Current Implementation
Identify what you're using from the legacy SDK:
- Content Repository -
Items(),GetById(),GetItemsByIDs() - Page Management - Routes in
global.asax - URL Redirects - Built-in redirect handling
- Content Models - Manual class definitions
Step 2: Set Up the New Project
- Clone the new starter (Blazor or MVC)
- Configure your API keys
- 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
| Legacy | Modern |
|---|---|
repository.Items<T>("reference") | fetchApi.GetTypedContentList<T>("reference", locale) |
repository.GetById<T>(id) | fetchApi.GetTypedContentItem<T>(id, locale) |
| LINQ filtering | API filter parameter |
| Synchronous | Async/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 Method | Modern 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/A | GetContentByGraphQL<T>(query, objName, locale) |
| N/A | GetTypedPage(pageId, locale) |
| N/A | GetSitemapFlat(channel, locale) |
| N/A | GetUrlRedirects(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:
- Check the GitHub Issues
- Join the Agility CMS Community
- 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
In this Article:
Was this article helpful?