# Migration Guide

> Source: https://agilitycms.com/docs/dotNet/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:

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

```bash
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

```csharp
// 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

```csharp
// 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"
        );
    }
}
```

```csharp
// 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

```csharp
// Global.asax.cs
protected void Application_Start()
{
    // Agility route registration
    AgilityRouteConfig.RegisterRoutes(RouteTable.Routes);
}
```

### After: Middleware + Dependency Injection

```csharp
// 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

```csharp
// Program.cs - Add only if you need redirects
app.UseAgilityRedirects();
```

Or create custom redirect logic:

```csharp
// 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

```csharp
// 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)

```csharp
// 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:

```bash
# Install the CLI
dotnet tool install -g agility-cli

# Generate models
agility models generate --output ./Models
```

## Migrating Views

### Before: MVC 4/5 Views

```razor
@* 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

```razor
@* 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

```csharp
// 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);
    }
}
```

```razor
@* 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

```xml
<appSettings>
    <add key="Agility.ContentAccessor.InstanceGuid" value="..." />
    <add key="Agility.ContentAccessor.ApiKey" value="..." />
</appSettings>
```

### After: appsettings.json

```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](https://github.com/agility/agilitycms-dotnet-starter/issues)
2. Join the [Agility CMS Community](https://agilitycms.com/community)
3. Contact [Agility Support](https://agilitycms.com/support)

## Next Steps

- [Getting Started](/docs/dotNet/getting-started) - Set up the new project
- [Blazor Starter](/docs/dotNet/blazor-starter) - Blazor-specific features
- [MVC Starter](/docs/dotNet/mvc-starter) - MVC-specific features
- [Fetching Content](/docs/dotNet/fetching-content) - New API methods
- [Configuration](/docs/dotNet/configuration) - Configuration options
