Tutorials
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.
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 |
Identify what you're using from the legacy SDK:
Items(), GetById(), GetItemsByIDs()global.asaxgit 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
Your biggest task is converting views/components. The content models and logic remain similar.
// 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);
}
}
// 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);
}
}
| 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 |
// Global.asax.cs
protected void Application_Start()
{
// Agility route registration
AgilityRouteConfig.RegisterRoutes(RouteTable.Routes);
}
// 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}");
DynamicRouteValueTransformerURL redirects were always enabled with no customization.
// 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);
}
}
// 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; }
}
// 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; }
}
Generate models from your Agility content definitions:
# Install the CLI
dotnet tool install -g agility-cli
# Generate models
agility models generate --output ./Models
@* 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>
@* 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");
}
}
// 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>
| 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) |
<appSettings>
<add key="Agility.ContentAccessor.InstanceGuid" value="..." />
<add key="Agility.ContentAccessor.ApiKey" value="..." />
</appSettings>
{
"AppSettings": {
"InstanceGUID": "your-guid",
"SecurityKey": "your-key",
"FetchAPIKey": "defaultlive.your-key",
"PreviewAPIKey": "defaultpreview.your-key",
"Locales": "en-us",
"ChannelName": "website",
"CacheInMinutes": 5
}
}
GetTypedContentList<T>, GetTypedPage, etc.appsettings.local.jsonModels/AgilityModels.csIf you encounter issues during migration: