← Back to Blog

Building End-to-End Solutions with Claude Code: From .NET MAUI to Azure DevOps

Michael Mallit
November 20, 2024
14 min read
Building End-to-End Solutions with Claude Code: From .NET MAUI to Azure DevOps

A comprehensive guide to modern software development workflows using Claude Code for AI-assisted development. Learn how I build complete solutions spanning .NET MAUI mobile apps, ASP.NET Core APIs, comprehensive testing, and Azure DevOps CI/CD pipelines with TestFlight distribution.

Introduction: The Modern Development Workflow

The software development landscape has transformed dramatically with the emergence of AI-powered coding assistants. Over the past year, I've integrated Claude Code into my development workflow at MallitLabs, and it has fundamentally changed how I approach building complete solutions. From initial project scaffolding to production deployment, AI assistance has become an invaluable partner in my engineering process.

In this comprehensive guide, I'll walk you through my end-to-end workflow for building modern applications: creating a .NET MAUI mobile application, developing an ASP.NET Core Web API backend, implementing comprehensive testing strategies, and establishing robust CI/CD pipelines in Azure DevOps—all with Claude Code as my AI development companion.

This isn't theoretical exploration. These are the actual patterns, practices, and workflows I use daily when building production applications for clients. You'll see real examples, learn about the challenges I've encountered, and discover how AI-assisted development can accelerate your work without sacrificing quality.

Why This Workflow Matters

Before diving into the technical details, let me explain why this particular technology stack and workflow has become my go-to approach for mobile-first applications:

  • .NET MAUI enables true cross-platform development with a single codebase targeting iOS, Android, Windows, and macOS
  • ASP.NET Core provides enterprise-grade API performance with excellent tooling and ecosystem support
  • Azure DevOps offers comprehensive DevOps capabilities with deep Microsoft stack integration
  • Claude Code accelerates development while maintaining code quality through intelligent assistance

The combination delivers professional-grade results efficiently, which is crucial when you're building custom solutions for clients who need both speed and reliability.

Part 1: Project Setup with Claude Code

Starting a new project traditionally involves dozens of decisions, boilerplate code, and configuration files. Claude Code transforms this from a tedious checklist into an interactive conversation where I can focus on architectural decisions while the AI handles the mechanical details.

Creating the .NET MAUI Mobile Application

I start every mobile project with a conversation with Claude Code about the project requirements. Here's how a typical exchange goes:

"I need to create a new .NET MAUI application targeting iOS and Android. The app will need to authenticate users, display a list of items from an API, and support offline mode with local caching. Set up the project with MVVM architecture using CommunityToolkit.Mvvm."

Claude Code doesn't just create a basic template—it scaffolds a complete project structure aligned with best practices:

dotnet new maui -n MyApp.Mobile
cd MyApp.Mobile
dotnet add package CommunityToolkit.Mvvm
dotnet add package CommunityToolkit.Maui
dotnet add package Microsoft.Extensions.Http
dotnet add package SQLite-net-pcl

But the real power comes next. Claude Code generates the foundational architecture:

// Models/Item.cs
public class Item
{
    [PrimaryKey, AutoIncrement]
    public int Id { get; set; }
    public string Title { get; set; }
    public string Description { get; set; }
    public DateTime LastModified { get; set; }
    public bool IsSynced { get; set; }
}

// Services/IItemService.cs
public interface IItemService
{
    Task> GetItemsAsync(bool forceRefresh = false);
    Task GetItemAsync(int id);
    Task AddItemAsync(Item item);
    Task UpdateItemAsync(Item item);
    Task DeleteItemAsync(int id);
    Task SyncAsync();
}

// Services/ItemService.cs
public class ItemService : IItemService
{
    private readonly HttpClient _httpClient;
    private readonly SQLiteAsyncConnection _database;
    private readonly IConnectivity _connectivity;

    public ItemService(
        HttpClient httpClient,
        SQLiteAsyncConnection database,
        IConnectivity connectivity)
    {
        _httpClient = httpClient;
        _database = database;
        _connectivity = connectivity;
    }

    public async Task> GetItemsAsync(bool forceRefresh = false)
    {
        // Check connectivity
        if (_connectivity.NetworkAccess == NetworkAccess.Internet && forceRefresh)
        {
            try
            {
                // Fetch from API
                var items = await _httpClient.GetFromJsonAsync>("api/items");
                
                // Update local cache
                await _database.DeleteAllAsync();
                await _database.InsertAllAsync(items);
                
                return items;
            }
            catch (Exception ex)
            {
                // Log error, fall back to cache
            }
        }
        
        // Return cached data
        return await _database.Table().ToListAsync();
    }
    
    // Additional methods...
}

// ViewModels/ItemsViewModel.cs
public partial class ItemsViewModel : ObservableObject
{
    private readonly IItemService _itemService;
    
    [ObservableProperty]
    private ObservableCollection items;
    
    [ObservableProperty]
    private bool isRefreshing;
    
    public ItemsViewModel(IItemService itemService)
    {
        _itemService = itemService;
        Items = new ObservableCollection();
    }
    
    [RelayCommand]
    private async Task LoadItemsAsync()
    {
        IsRefreshing = true;
        try
        {
            var items = await _itemService.GetItemsAsync(forceRefresh: true);
            Items.Clear();
            foreach (var item in items)
            {
                Items.Add(item);
            }
        }
        finally
        {
            IsRefreshing = false;
        }
    }
    
    [RelayCommand]
    private async Task AddItemAsync()
    {
        // Navigate to add item page
    }
}

What would have taken me an hour or more of manual coding is generated in minutes, and it's production-ready code following current best practices with proper dependency injection, error handling, and MVVM patterns.

Building the ASP.NET Core Web API

Next, I ask Claude Code to set up the backend API. The conversation continues:

"Create an ASP.NET Core Web API project with Entity Framework Core using SQL Server. Include authentication with JWT tokens, implement CRUD endpoints for items, add Swagger documentation, and set up health checks."

Claude Code scaffolds the complete API structure:

dotnet new webapi -n MyApp.Api
cd MyApp.Api
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
dotnet add package Swashbuckle.AspNetCore
dotnet add package Microsoft.AspNetCore.Diagnostics.HealthChecks

Then generates a complete API implementation:

// Models/Item.cs
public class Item
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Description { get; set; }
    public DateTime CreatedAt { get; set; }
    public DateTime LastModified { get; set; }
    public string UserId { get; set; }
}

// Data/ApplicationDbContext.cs
public class ApplicationDbContext : DbContext
{
    public ApplicationDbContext(DbContextOptions options)
        : base(options)
    {
    }

    public DbSet Items { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity(entity =>
        {
            entity.HasKey(e => e.Id);
            entity.Property(e => e.Title).IsRequired().HasMaxLength(200);
            entity.Property(e => e.Description).HasMaxLength(1000);
            entity.Property(e => e.UserId).IsRequired();
            entity.HasIndex(e => e.UserId);
            entity.HasIndex(e => e.LastModified);
        });
    }
}

// Controllers/ItemsController.cs
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class ItemsController : ControllerBase
{
    private readonly ApplicationDbContext _context;
    private readonly ILogger _logger;

    public ItemsController(
        ApplicationDbContext context,
        ILogger logger)
    {
        _context = context;
        _logger = logger;
    }

    [HttpGet]
    public async Task>> GetItems(
        [FromQuery] DateTime? since = null)
    {
        var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
        
        var query = _context.Items
            .Where(i => i.UserId == userId);
            
        if (since.HasValue)
        {
            query = query.Where(i => i.LastModified > since.Value);
        }
        
        var items = await query
            .OrderByDescending(i => i.LastModified)
            .ToListAsync();
            
        return Ok(items);
    }

    [HttpGet("{id}")]
    public async Task> GetItem(int id)
    {
        var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
        
        var item = await _context.Items
            .FirstOrDefaultAsync(i => i.Id == id && i.UserId == userId);

        if (item == null)
        {
            return NotFound();
        }

        return Ok(item);
    }

    [HttpPost]
    public async Task> CreateItem([FromBody] Item item)
    {
        var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
        
        item.UserId = userId;
        item.CreatedAt = DateTime.UtcNow;
        item.LastModified = DateTime.UtcNow;

        _context.Items.Add(item);
        await _context.SaveChangesAsync();

        return CreatedAtAction(
            nameof(GetItem),
            new { id = item.Id },
            item);
    }

    [HttpPut("{id}")]
    public async Task UpdateItem(int id, [FromBody] Item item)
    {
        var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
        
        if (id != item.Id)
        {
            return BadRequest();
        }

        var existingItem = await _context.Items
            .FirstOrDefaultAsync(i => i.Id == id && i.UserId == userId);

        if (existingItem == null)
        {
            return NotFound();
        }

        existingItem.Title = item.Title;
        existingItem.Description = item.Description;
        existingItem.LastModified = DateTime.UtcNow;

        await _context.SaveChangesAsync();

        return NoContent();
    }

    [HttpDelete("{id}")]
    public async Task DeleteItem(int id)
    {
        var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
        
        var item = await _context.Items
            .FirstOrDefaultAsync(i => i.Id == id && i.UserId == userId);

        if (item == null)
        {
            return NotFound();
        }

        _context.Items.Remove(item);
        await _context.SaveChangesAsync();

        return NoContent();
    }
}

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

// Add services
builder.Services.AddDbContext(options =>
    options.UseSqlServer(
        builder.Configuration.GetConnectionString("DefaultConnection")));

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = builder.Configuration["Jwt:Issuer"],
            ValidAudience = builder.Configuration["Jwt:Audience"],
            IssuerSigningKey = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]))
        };
    });

builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1", new OpenApiInfo
    {
        Title = "MyApp API",
        Version = "v1",
        Description = "API for MyApp mobile application"
    });
    
    c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
    {
        Description = "JWT Authorization header using the Bearer scheme",
        Name = "Authorization",
        In = ParameterLocation.Header,
        Type = SecuritySchemeType.ApiKey,
        Scheme = "Bearer"
    });
});

builder.Services.AddHealthChecks()
    .AddDbContextCheck();

var app = builder.Build();

// Configure middleware pipeline
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.MapHealthChecks("/health");

app.Run();

The API includes proper authentication, authorization checks ensuring users can only access their own data, efficient querying with optional delta sync support, comprehensive Swagger documentation, and health check endpoints for monitoring.

Setting Up Test Projects

Testing is non-negotiable in professional software development. I ask Claude Code to set up comprehensive test coverage:

"Create unit test and integration test projects. For unit tests, use xUnit with Moq for mocking. For integration tests, use WebApplicationFactory with an in-memory database. Include examples testing the ItemsController and ItemService."

dotnet new xunit -n MyApp.Tests.Unit
cd MyApp.Tests.Unit
dotnet add package Moq
dotnet add package FluentAssertions
dotnet add reference ../MyApp.Api/MyApp.Api.csproj

cd ..
dotnet new xunit -n MyApp.Tests.Integration
cd MyApp.Tests.Integration
dotnet add package Microsoft.AspNetCore.Mvc.Testing
dotnet add package Microsoft.EntityFrameworkCore.InMemory
dotnet add reference ../MyApp.Api/MyApp.Api.csproj

Claude Code then generates comprehensive test suites:

// Unit Tests: ItemsControllerTests.cs
public class ItemsControllerTests
{
    private readonly Mock _mockContext;
    private readonly Mock> _mockLogger;
    private readonly ItemsController _controller;

    public ItemsControllerTests()
    {
        _mockContext = new Mock();
        _mockLogger = new Mock>();
        _controller = new ItemsController(
            _mockContext.Object,
            _mockLogger.Object);
            
        // Setup mock user
        var user = new ClaimsPrincipal(new ClaimsIdentity(new Claim[]
        {
            new Claim(ClaimTypes.NameIdentifier, "test-user-123")
        }, "mock"));
        
        _controller.ControllerContext = new ControllerContext()
        {
            HttpContext = new DefaultHttpContext() { User = user }
        };
    }

    [Fact]
    public async Task GetItems_ReturnsUserItems_WhenItemsExist()
    {
        // Arrange
        var items = new List
        {
            new Item
            {
                Id = 1,
                Title = "Test Item",
                UserId = "test-user-123",
                LastModified = DateTime.UtcNow
            }
        };
        
        var mockSet = CreateMockDbSet(items);
        _mockContext.Setup(c => c.Items).Returns(mockSet.Object);

        // Act
        var result = await _controller.GetItems();

        // Assert
        result.Result.Should().BeOfType();
        var okResult = result.Result as OkObjectResult;
        var returnedItems = okResult.Value as IEnumerable;
        returnedItems.Should().HaveCount(1);
        returnedItems.First().Title.Should().Be("Test Item");
    }

    [Fact]
    public async Task CreateItem_ReturnsCreatedItem_WithGeneratedId()
    {
        // Arrange
        var newItem = new Item
        {
            Title = "New Item",
            Description = "Test Description"
        };
        
        _mockContext.Setup(c => c.Items.Add(It.IsAny()));
        _mockContext.Setup(c => c.SaveChangesAsync(default))
            .ReturnsAsync(1);

        // Act
        var result = await _controller.CreateItem(newItem);

        // Assert
        result.Result.Should().BeOfType();
        var createdResult = result.Result as CreatedAtActionResult;
        var createdItem = createdResult.Value as Item;
        createdItem.UserId.Should().Be("test-user-123");
        createdItem.CreatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1));
    }
    
    private Mock> CreateMockDbSet(List data) where T : class
    {
        var queryable = data.AsQueryable();
        var mockSet = new Mock>();
        
        mockSet.As>().Setup(m => m.Provider)
            .Returns(queryable.Provider);
        mockSet.As>().Setup(m => m.Expression)
            .Returns(queryable.Expression);
        mockSet.As>().Setup(m => m.ElementType)
            .Returns(queryable.ElementType);
        mockSet.As>().Setup(m => m.GetEnumerator())
            .Returns(queryable.GetEnumerator());
            
        return mockSet;
    }
}

// Integration Tests: ItemsApiTests.cs
public class ItemsApiTests : IClassFixture>
{
    private readonly HttpClient _client;
    private readonly WebApplicationFactory _factory;

    public ItemsApiTests(WebApplicationFactory factory)
    {
        _factory = factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureServices(services =>
            {
                // Remove the real database
                var descriptor = services.SingleOrDefault(
                    d => d.ServiceType == typeof(DbContextOptions));
                    
                if (descriptor != null)
                {
                    services.Remove(descriptor);
                }

                // Add in-memory database
                services.AddDbContext(options =>
                {
                    options.UseInMemoryDatabase("TestDb");
                });
            });
        });
        
        _client = _factory.CreateClient();
    }

    [Fact]
    public async Task GetItems_ReturnsSuccessStatusCode()
    {
        // Arrange
        var token = await GetAuthTokenAsync();
        _client.DefaultRequestHeaders.Authorization = 
            new AuthenticationHeaderValue("Bearer", token);

        // Act
        var response = await _client.GetAsync("/api/items");

        // Assert
        response.EnsureSuccessStatusCode();
        var items = await response.Content.ReadFromJsonAsync>();
        items.Should().NotBeNull();
    }

    [Fact]
    public async Task CreateItem_ThenGetItem_ReturnsCreatedItem()
    {
        // Arrange
        var token = await GetAuthTokenAsync();
        _client.DefaultRequestHeaders.Authorization = 
            new AuthenticationHeaderValue("Bearer", token);
            
        var newItem = new Item
        {
            Title = "Integration Test Item",
            Description = "Created during integration test"
        };

        // Act - Create
        var createResponse = await _client.PostAsJsonAsync("/api/items", newItem);
        createResponse.EnsureSuccessStatusCode();
        var createdItem = await createResponse.Content.ReadFromJsonAsync();

        // Act - Retrieve
        var getResponse = await _client.GetAsync($"/api/items/{createdItem.Id}");
        var retrievedItem = await getResponse.Content.ReadFromJsonAsync();

        // Assert
        retrievedItem.Should().NotBeNull();
        retrievedItem.Title.Should().Be(newItem.Title);
        retrievedItem.Description.Should().Be(newItem.Description);
    }
    
    private async Task GetAuthTokenAsync()
    {
        // Implementation to get test authentication token
        return "test-token";
    }
}

These comprehensive tests ensure code quality and catch regressions early. The unit tests verify business logic in isolation, while integration tests validate the entire API pipeline including routing, authentication, database operations, and serialization.

Part 2: Development Workflow with Claude Code

Project setup is just the beginning. The real value of Claude Code emerges during the daily development cycle: implementing features, debugging issues, and refining code quality.

Feature Implementation: Real-World Example

Let me share a recent scenario. I needed to add offline synchronization with conflict resolution to the mobile app. This is complex functionality involving:

  • Tracking local changes while offline
  • Detecting conflicts when syncing with the server
  • Implementing a merge strategy for conflicting changes
  • Providing user feedback during the sync process

Instead of spending hours researching patterns and implementations, I had a conversation with Claude Code:

"I need to implement offline sync with conflict resolution. When the app comes back online, it should sync local changes to the API. If the server has newer changes, implement a 'last write wins' strategy but log conflicts. Add a visual indicator showing sync status."

Claude Code analyzed the existing codebase context and generated a complete implementation:

// Models/SyncItem.cs
public class SyncItem
{
    [PrimaryKey, AutoIncrement]
    public int LocalId { get; set; }
    public int? ServerId { get; set; }
    public string ItemJson { get; set; }
    public DateTime LocalModifiedAt { get; set; }
    public SyncAction Action { get; set; }
    public bool IsSynced { get; set; }
}

public enum SyncAction
{
    Create,
    Update,
    Delete
}

// Services/SyncService.cs
public class SyncService : ISyncService
{
    private readonly HttpClient _httpClient;
    private readonly SQLiteAsyncConnection _database;
    private readonly IConnectivity _connectivity;
    private readonly ILogger _logger;

    public SyncStatus Status { get; private set; }

    public async Task SyncAsync()
    {
        if (_connectivity.NetworkAccess != NetworkAccess.Internet)
        {
            return SyncResult.Offline;
        }

        Status = SyncStatus.Syncing;
        var result = new SyncResult();

        try
        {
            // Get pending sync items
            var pendingItems = await _database.Table()
                .Where(s => !s.IsSynced)
                .ToListAsync();

            foreach (var syncItem in pendingItems)
            {
                try
                {
                    var item = JsonSerializer.Deserialize(syncItem.ItemJson);

                    switch (syncItem.Action)
                    {
                        case SyncAction.Create:
                            var created = await _httpClient.PostAsJsonAsync(
                                "api/items", item);
                            created.EnsureSuccessStatusCode();
                            result.Created++;
                            break;

                        case SyncAction.Update:
                            var updated = await _httpClient.PutAsJsonAsync(
                                $"api/items/{syncItem.ServerId}", item);
                            
                            if (updated.StatusCode == HttpStatusCode.Conflict)
                            {
                                // Handle conflict - last write wins
                                _logger.LogWarning(
                                    "Conflict detected for item {Id}. Local changes will overwrite server.",
                                    syncItem.ServerId);
                                    
                                // Force update
                                updated = await _httpClient.PutAsJsonAsync(
                                    $"api/items/{syncItem.ServerId}?force=true", item);
                                result.Conflicts++;
                            }
                            
                            updated.EnsureSuccessStatusCode();
                            result.Updated++;
                            break;

                        case SyncAction.Delete:
                            var deleted = await _httpClient.DeleteAsync(
                                $"api/items/{syncItem.ServerId}");
                            deleted.EnsureSuccessStatusCode();
                            result.Deleted++;
                            break;
                    }

                    // Mark as synced
                    syncItem.IsSynced = true;
                    await _database.UpdateAsync(syncItem);
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, 
                        "Failed to sync item {LocalId}", syncItem.LocalId);
                    result.Errors++;
                }
            }

            // Pull latest from server
            var lastSync = await GetLastSyncTimeAsync();
            var serverItems = await _httpClient.GetFromJsonAsync>(
                $"api/items?since={lastSync:O}");

            foreach (var serverItem in serverItems)
            {
                var localItem = await _database.Table()
                    .FirstOrDefaultAsync(i => i.Id == serverItem.Id);

                if (localItem == null)
                {
                    await _database.InsertAsync(serverItem);
                    result.Downloaded++;
                }
                else if (serverItem.LastModified > localItem.LastModified)
                {
                    await _database.UpdateAsync(serverItem);
                    result.Downloaded++;
                }
            }

            await SetLastSyncTimeAsync(DateTime.UtcNow);
            Status = SyncStatus.Idle;
            
            return result;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Sync failed");
            Status = SyncStatus.Error;
            throw;
        }
    }
}

// ViewModels/SyncIndicatorViewModel.cs
public partial class SyncIndicatorViewModel : ObservableObject
{
    private readonly ISyncService _syncService;
    private readonly IConnectivity _connectivity;

    [ObservableProperty]
    private string syncStatusText;

    [ObservableProperty]
    private Color syncStatusColor;

    [ObservableProperty]
    private bool isSyncing;

    public SyncIndicatorViewModel(
        ISyncService syncService,
        IConnectivity connectivity)
    {
        _syncService = syncService;
        _connectivity = connectivity;

        // Monitor connectivity changes
        _connectivity.ConnectivityChanged += OnConnectivityChanged;
        
        UpdateSyncStatus();
    }

    private void OnConnectivityChanged(object sender, ConnectivityChangedEventArgs e)
    {
        MainThread.BeginInvokeOnMainThread(async () =>
        {
            UpdateSyncStatus();
            
            if (e.NetworkAccess == NetworkAccess.Internet)
            {
                await SyncAsync();
            }
        });
    }

    [RelayCommand]
    private async Task SyncAsync()
    {
        if (IsSyncing) return;

        IsSyncing = true;
        SyncStatusText = "Syncing...";
        SyncStatusColor = Colors.Orange;

        try
        {
            var result = await _syncService.SyncAsync();
            
            if (result.Errors > 0)
            {
                SyncStatusText = $"Synced with {result.Errors} errors";
                SyncStatusColor = Colors.Yellow;
            }
            else if (result.Conflicts > 0)
            {
                SyncStatusText = $"Synced ({result.Conflicts} conflicts resolved)";
                SyncStatusColor = Colors.Yellow;
            }
            else
            {
                SyncStatusText = "Synced";
                SyncStatusColor = Colors.Green;
            }
        }
        catch (Exception ex)
        {
            SyncStatusText = "Sync failed";
            SyncStatusColor = Colors.Red;
        }
        finally
        {
            IsSyncing = false;
            
            // Reset status after delay
            await Task.Delay(3000);
            UpdateSyncStatus();
        }
    }

    private void UpdateSyncStatus()
    {
        if (_connectivity.NetworkAccess == NetworkAccess.Internet)
        {
            SyncStatusText = "Online";
            SyncStatusColor = Colors.Green;
        }
        else
        {
            SyncStatusText = "Offline";
            SyncStatusColor = Colors.Gray;
        }
    }
}

This complete implementation would have taken me several hours to research, design, and code. Claude Code delivered it in minutes, and it includes proper error handling, logging, conflict resolution, and user feedback—all the production-ready details you need.

Debugging with Claude Code

AI assistance really shines during debugging. Recently, I encountered a perplexing issue where API requests were failing intermittently on iOS but working fine on Android and in testing.

I copied the relevant code and error logs into Claude Code:

"I'm getting intermittent network failures on iOS. The error is 'The request timed out' but it's inconsistent. Here's my HttpClient setup and the code making requests..."

Claude Code immediately identified the issue:

"The problem is likely iOS's App Transport Security (ATS) combined with your HttpClient timeout configuration. iOS enforces stricter network security and your timeout might be too aggressive for poor network conditions. Additionally, you're creating a new HttpClient for each request, which exhausts ports on iOS more quickly than Android."

It provided the fix:

// MauiProgram.cs - Proper HttpClient registration
builder.Services.AddHttpClient(client =>
{
    client.BaseAddress = new Uri("https://api.myapp.com/");
    client.Timeout = TimeSpan.FromSeconds(30); // Increased from 10
    client.DefaultRequestHeaders.Add("Accept", "application/json");
})
.ConfigurePrimaryHttpMessageHandler(() =>
{
    return new HttpClientHandler
    {
        ServerCertificateCustomValidationCallback = 
            HttpClientHandler.DangerousAcceptAnyServerCertificateValidator,
        // Only for development!
        #if DEBUG
        AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate
        #endif
    };
})
.AddPolicyHandler(GetRetryPolicy());

static IAsyncPolicy GetRetryPolicy()
{
    return HttpPolicyExtensions
        .HandleTransientHttpError()
        .OrResult(msg => msg.StatusCode == System.Net.HttpStatusCode.TooManyRequests)
        .WaitAndRetryAsync(
            retryCount: 3,
            sleepDurationProvider: retryAttempt => 
                TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
            onRetry: (outcome, timespan, retryCount, context) =>
            {
                Debug.WriteLine($"Retry {retryCount} after {timespan.TotalSeconds}s");
            });
}

This solution not only fixed the immediate issue but improved the overall resilience of the app with proper retry policies and HttpClient lifecycle management.

Code Review and Refactoring

I regularly ask Claude Code to review code for potential improvements:

"Review this ItemService implementation for potential issues, performance problems, or better patterns I should consider."

Claude Code provides thoughtful analysis:

  • "Consider implementing the Repository pattern to separate data access concerns"
  • "The SyncAsync method is doing too much - extract conflict resolution into a separate strategy class"
  • "You're not canceling ongoing sync operations when connectivity changes - this could lead to race conditions"
  • "Consider adding telemetry to track sync performance and failure rates"

These suggestions come with code examples showing exactly how to implement the improvements. It's like having a senior engineer reviewing your code in real-time.

Part 3: Azure DevOps Setup

With the code in place, the next step is establishing the DevOps infrastructure. I use Azure DevOps for its excellent integration with the Microsoft development stack and comprehensive CI/CD capabilities.

Creating the Azure DevOps Project

I start by creating a new project in Azure DevOps, then initialize the Git repositories:

git init
git remote add origin https://dev.azure.com/mallitlabs/MyApp/_git/MyApp
git add .
git commit -m "Initial commit - project structure"
git push -u origin main

I organize the repository structure to support multiple pipelines:

MyApp/
├── src/
│   ├── MyApp.Mobile/
│   ├── MyApp.Api/
│   ├── MyApp.Tests.Unit/
│   └── MyApp.Tests.Integration/
├── pipelines/
│   ├── api-pipeline.yml
│   ├── ios-pipeline.yml
│   └── android-pipeline.yml
├── .gitignore
└── README.md

Infrastructure as Code

I ask Claude Code to help set up Azure resources:

"Create an Azure CLI script to provision the infrastructure needed for this application: App Service for the API, SQL Database, Application Insights for monitoring, and Key Vault for secrets."

#!/bin/bash

# Variables
RG_NAME="myapp-prod-rg"
LOCATION="eastus"
APP_SERVICE_PLAN="myapp-asp"
APP_SERVICE_NAME="myapp-api"
SQL_SERVER="myapp-sqlserver"
SQL_DB="myapp-db"
KEY_VAULT="myapp-kv"
APP_INSIGHTS="myapp-insights"

# Create resource group
az group create \
  --name $RG_NAME \
  --location $LOCATION

# Create App Service Plan (Linux, B1 tier)
az appservice plan create \
  --name $APP_SERVICE_PLAN \
  --resource-group $RG_NAME \
  --is-linux \
  --sku B1

# Create Web App
az webapp create \
  --name $APP_SERVICE_NAME \
  --resource-group $RG_NAME \
  --plan $APP_SERVICE_PLAN \
  --runtime "DOTNET|8.0"

# Create SQL Server
az sql server create \
  --name $SQL_SERVER \
  --resource-group $RG_NAME \
  --location $LOCATION \
  --admin-user sqladmin \
  --admin-password "P@ssw0rd123!"

# Create SQL Database
az sql db create \
  --name $SQL_DB \
  --server $SQL_SERVER \
  --resource-group $RG_NAME \
  --service-objective S0

# Configure firewall to allow Azure services
az sql server firewall-rule create \
  --server $SQL_SERVER \
  --resource-group $RG_NAME \
  --name AllowAzureServices \
  --start-ip-address 0.0.0.0 \
  --end-ip-address 0.0.0.0

# Create Key Vault
az keyvault create \
  --name $KEY_VAULT \
  --resource-group $RG_NAME \
  --location $LOCATION

# Create Application Insights
az monitor app-insights component create \
  --app $APP_INSIGHTS \
  --location $LOCATION \
  --resource-group $RG_NAME \
  --application-type web

# Get connection strings
SQL_CONNECTION=$(az sql db show-connection-string \
  --client ado.net \
  --server $SQL_SERVER \
  --name $SQL_DB \
  --output tsv)

INSIGHTS_KEY=$(az monitor app-insights component show \
  --app $APP_INSIGHTS \
  --resource-group $RG_NAME \
  --query instrumentationKey \
  --output tsv)

# Store secrets in Key Vault
az keyvault secret set \
  --vault-name $KEY_VAULT \
  --name "SqlConnectionString" \
  --value "$SQL_CONNECTION"

az keyvault secret set \
  --vault-name $KEY_VAULT \
  --name "ApplicationInsightsKey" \
  --value "$INSIGHTS_KEY"

# Grant Web App access to Key Vault
SYSTEM_IDENTITY=$(az webapp identity assign \
  --name $APP_SERVICE_NAME \
  --resource-group $RG_NAME \
  --query principalId \
  --output tsv)

az keyvault set-policy \
  --name $KEY_VAULT \
  --object-id $SYSTEM_IDENTITY \
  --secret-permissions get list

echo "Infrastructure provisioned successfully!"

Part 4: CI/CD Pipelines

Now comes the crucial part: automating build, test, and deployment processes. I'll share the actual pipeline configurations I use.

API Pipeline

The API pipeline builds the .NET application, runs tests, and deploys to Azure App Service:

# pipelines/api-pipeline.yml

trigger:
  branches:
    include:
      - main
      - develop
  paths:
    include:
      - src/MyApp.Api/**
      - src/MyApp.Tests.Unit/**
      - src/MyApp.Tests.Integration/**

pool:
  vmImage: 'ubuntu-latest'

variables:
  buildConfiguration: 'Release'
  dotnetSdkVersion: '8.x'

stages:
  - stage: Build
    displayName: 'Build and Test'
    jobs:
      - job: BuildAndTest
        displayName: 'Build and Test API'
        steps:
          - task: UseDotNet@2
            displayName: 'Install .NET SDK'
            inputs:
              packageType: 'sdk'
              version: $(dotnetSdkVersion)

          - task: DotNetCoreCLI@2
            displayName: 'Restore Dependencies'
            inputs:
              command: 'restore'
              projects: 'src/**/*.csproj'

          - task: DotNetCoreCLI@2
            displayName: 'Build Solution'
            inputs:
              command: 'build'
              projects: 'src/**/*.csproj'
              arguments: '--configuration $(buildConfiguration) --no-restore'

          - task: DotNetCoreCLI@2
            displayName: 'Run Unit Tests'
            inputs:
              command: 'test'
              projects: 'src/MyApp.Tests.Unit/**/*.csproj'
              arguments: '--configuration $(buildConfiguration) --no-build --collect:"XPlat Code Coverage" --logger trx'
              publishTestResults: true

          - task: DotNetCoreCLI@2
            displayName: 'Run Integration Tests'
            inputs:
              command: 'test'
              projects: 'src/MyApp.Tests.Integration/**/*.csproj'
              arguments: '--configuration $(buildConfiguration) --no-build --collect:"XPlat Code Coverage" --logger trx'
              publishTestResults: true

          - task: PublishCodeCoverageResults@2
            displayName: 'Publish Code Coverage'
            inputs:
              summaryFileLocation: '$(Agent.TempDirectory)/**/*coverage.cobertura.xml'

          - task: DotNetCoreCLI@2
            displayName: 'Publish API'
            inputs:
              command: 'publish'
              projects: 'src/MyApp.Api/MyApp.Api.csproj'
              arguments: '--configuration $(buildConfiguration) --output $(Build.ArtifactStagingDirectory)/api'
              zipAfterPublish: true

          - task: PublishBuildArtifacts@1
            displayName: 'Publish Build Artifacts'
            inputs:
              pathToPublish: '$(Build.ArtifactStagingDirectory)'
              artifactName: 'drop'

  - stage: DeployDev
    displayName: 'Deploy to Development'
    dependsOn: Build
    condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/develop'))
    jobs:
      - deployment: DeployToDev
        displayName: 'Deploy to Dev Environment'
        environment: 'Development'
        strategy:
          runOnce:
            deploy:
              steps:
                - task: AzureWebApp@1
                  displayName: 'Deploy to Azure App Service (Dev)'
                  inputs:
                    azureSubscription: 'Azure-Service-Connection'
                    appName: 'myapp-api-dev'
                    package: '$(Pipeline.Workspace)/drop/api/*.zip'
                    appType: 'webAppLinux'
                    runtimeStack: 'DOTNETCORE|8.0'

                - task: AzureCLI@2
                  displayName: 'Run Database Migrations'
                  inputs:
                    azureSubscription: 'Azure-Service-Connection'
                    scriptType: 'bash'
                    scriptLocation: 'inlineScript'
                    inlineScript: |
                      # Download EF Core tools
                      dotnet tool install --global dotnet-ef
                      
                      # Run migrations
                      dotnet ef database update \
                        --project src/MyApp.Api/MyApp.Api.csproj \
                        --connection "$(DEV_SQL_CONNECTION_STRING)"

  - stage: DeployProd
    displayName: 'Deploy to Production'
    dependsOn: Build
    condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
    jobs:
      - deployment: DeployToProd
        displayName: 'Deploy to Production Environment'
        environment: 'Production'
        strategy:
          runOnce:
            deploy:
              steps:
                - task: AzureWebApp@1
                  displayName: 'Deploy to Azure App Service (Prod)'
                  inputs:
                    azureSubscription: 'Azure-Service-Connection'
                    appName: 'myapp-api-prod'
                    package: '$(Pipeline.Workspace)/drop/api/*.zip'
                    appType: 'webAppLinux'
                    runtimeStack: 'DOTNETCORE|8.0'
                    deploymentMethod: 'zipDeploy'

                - task: AzureAppServiceSettings@1
                  displayName: 'Configure App Settings'
                  inputs:
                    azureSubscription: 'Azure-Service-Connection'
                    appName: 'myapp-api-prod'
                    resourceGroupName: 'myapp-prod-rg'
                    appSettings: |
                      [
                        {
                          "name": "ASPNETCORE_ENVIRONMENT",
                          "value": "Production",
                          "slotSetting": false
                        },
                        {
                          "name": "ApplicationInsights__InstrumentationKey",
                          "value": "@Microsoft.KeyVault(SecretUri=$(KEY_VAULT_URI)secrets/ApplicationInsightsKey/)",
                          "slotSetting": false
                        }
                      ]

This pipeline includes comprehensive testing, code coverage reporting, artifact publishing, environment-specific deployments, and database migration execution.

iOS Pipeline with TestFlight Distribution

The iOS pipeline is more complex due to code signing requirements and TestFlight integration:

# pipelines/ios-pipeline.yml

trigger:
  branches:
    include:
      - main
      - develop
  paths:
    include:
      - src/MyApp.Mobile/**

pool:
  vmImage: 'macOS-latest'

variables:
  buildConfiguration: 'Release'
  dotnetVersion: '8.x'
  mauiVersion: '8.0.x'
  xcodeVersion: '15.1'

stages:
  - stage: BuildiOS
    displayName: 'Build iOS Application'
    jobs:
      - job: BuildiOSApp
        displayName: 'Build and Sign iOS App'
        steps:
          - task: UseDotNet@2
            displayName: 'Install .NET SDK'
            inputs:
              packageType: 'sdk'
              version: $(dotnetVersion)

          - task: Bash@3
            displayName: 'Install .NET MAUI Workload'
            inputs:
              targetType: 'inline'
              script: |
                dotnet workload install maui \
                  --source https://api.nuget.org/v3/index.json

          - task: InstallAppleCertificate@2
            displayName: 'Install Apple Certificate'
            inputs:
              certSecureFile: 'AppleDevelopment.p12'
              certPwd: '$(P12_PASSWORD)'
              keychain: 'temp'
              deleteCert: true

          - task: InstallAppleProvisioningProfile@1
            displayName: 'Install Provisioning Profile'
            inputs:
              provisioningProfileLocation: 'secureFiles'
              provProfileSecureFile: 'MyApp_AdHoc.mobileprovision'
              removeProfile: true

          - task: Bash@3
            displayName: 'Set Version Number'
            inputs:
              targetType: 'inline'
              script: |
                # Extract version from project file or set from pipeline
                VERSION="$(Build.BuildNumber)"
                BUILD_NUMBER="$(Build.BuildId)"
                
                # Update Info.plist with version
                /usr/libexec/PlistBuddy \
                  -c "Set :CFBundleShortVersionString $VERSION" \
                  src/MyApp.Mobile/Platforms/iOS/Info.plist
                  
                /usr/libexec/PlistBuddy \
                  -c "Set :CFBundleVersion $BUILD_NUMBER" \
                  src/MyApp.Mobile/Platforms/iOS/Info.plist

          - task: Bash@3
            displayName: 'Restore and Build iOS'
            inputs:
              targetType: 'inline'
              script: |
                cd src/MyApp.Mobile
                
                dotnet restore MyApp.Mobile.csproj \
                  -f net8.0-ios
                
                dotnet build MyApp.Mobile.csproj \
                  -f net8.0-ios \
                  -c $(buildConfiguration) \
                  /p:ArchiveOnBuild=true \
                  /p:EnableAssemblyILStripping=true \
                  /p:RuntimeIdentifier=ios-arm64 \
                  /p:CodesignKey="$(CODESIGN_KEY)" \
                  /p:CodesignProvision="$(PROVISIONING_PROFILE)" \
                  /p:BuildIpa=true

          - task: CopyFiles@2
            displayName: 'Copy IPA to Staging'
            inputs:
              Contents: '**/*.ipa'
              TargetFolder: '$(Build.ArtifactStagingDirectory)'
              flattenFolders: true

          - task: PublishBuildArtifacts@1
            displayName: 'Publish IPA Artifact'
            inputs:
              pathToPublish: '$(Build.ArtifactStagingDirectory)'
              artifactName: 'ios-app'

  - stage: TestFlight
    displayName: 'Distribute to TestFlight'
    dependsOn: BuildiOS
    condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
    jobs:
      - deployment: PublishToTestFlight
        displayName: 'Upload to TestFlight'
        environment: 'TestFlight'
        strategy:
          runOnce:
            deploy:
              steps:
                - task: AppStoreRelease@1
                  displayName: 'Upload to App Store Connect'
                  inputs:
                    serviceEndpoint: 'Apple-App-Store-Connection'
                    appIdentifier: 'com.mallitlabs.myapp'
                    ipaPath: '$(Pipeline.Workspace)/ios-app/*.ipa'
                    releaseTrack: 'TestFlight'
                    shouldSkipWaitingForProcessing: true
                    shouldSkipSubmission: false
                    shouldAutoRelease: false
                    appSpecificPassword: '$(APP_SPECIFIC_PASSWORD)'

                - task: Bash@3
                  displayName: 'Send Notification'
                  inputs:
                    targetType: 'inline'
                    script: |
                      # Send notification to team via webhook
                      curl -X POST $(TEAMS_WEBHOOK_URL) \
                        -H 'Content-Type: application/json' \
                        -d '{
                          "title": "New TestFlight Build Available",
                          "text": "MyApp iOS build $(Build.BuildNumber) has been uploaded to TestFlight.",
                          "themeColor": "0078D4"
                        }'

  - stage: InternalTesting
    displayName: 'Internal Testing Group'
    dependsOn: TestFlight
    condition: succeeded()
    jobs:
      - deployment: AddToInternalTesters
        displayName: 'Add Build to Internal Testers'
        environment: 'Internal-Testing'
        strategy:
          runOnce:
            deploy:
              steps:
                - task: Bash@3
                  displayName: 'Add to Internal Testing Group'
                  inputs:
                    targetType: 'inline'
                    script: |
                      # Use App Store Connect API to add build to testing group
                      # This requires pre-configured API key
                      
                      # Authenticate
                      TOKEN=$(curl -X POST \
                        https://api.appstoreconnect.apple.com/v1/tokens \
                        -H "Authorization: Bearer $(APP_STORE_API_KEY)" | \
                        jq -r '.token')
                      
                      # Get latest build
                      BUILD_ID=$(curl \
                        "https://api.appstoreconnect.apple.com/v1/builds?filter[app]=$(APP_ID)&sort=-version" \
                        -H "Authorization: Bearer $TOKEN" | \
                        jq -r '.data[0].id')
                      
                      # Add to internal testing group
                      curl -X POST \
                        https://api.appstoreconnect.apple.com/v1/betaBuildLocalizations \
                        -H "Authorization: Bearer $TOKEN" \
                        -H "Content-Type: application/json" \
                        -d '{
                          "data": {
                            "type": "betaBuildLocalizations",
                            "attributes": {
                              "whatsNew": "Build $(Build.BuildNumber) - $(Build.SourceVersionMessage)"
                            },
                            "relationships": {
                              "build": {
                                "data": {
                                  "type": "builds",
                                  "id": "'$BUILD_ID'"
                                }
                              }
                            }
                          }
                        }'

This comprehensive iOS pipeline handles the entire workflow from build to TestFlight distribution, including version management, code signing, artifact publishing, and automated tester notifications.

Part 5: Testing Strategy in CI/CD

Automated testing in the pipeline is crucial for maintaining quality at scale. Here's how I structure comprehensive test coverage:

Test Organization

I organize tests into multiple layers:

  • Unit Tests: Fast, isolated tests covering business logic
  • Integration Tests: Testing API endpoints with real database (in-memory for CI)
  • UI Tests: Automated UI testing for critical mobile app workflows

Adding UI Tests to the iOS Pipeline

For mobile apps, UI testing catches issues that unit and integration tests miss:

# Additional stage in ios-pipeline.yml

  - stage: UITesting
    displayName: 'Run UI Tests'
    dependsOn: BuildiOS
    condition: succeeded()
    jobs:
      - job: RunUITests
        displayName: 'Execute UI Test Suite'
        steps:
          - task: Bash@3
            displayName: 'Boot iOS Simulator'
            inputs:
              targetType: 'inline'
              script: |
                # List available simulators
                xcrun simctl list devices
                
                # Boot iPhone 15 simulator
                DEVICE_ID=$(xcrun simctl list devices | \
                  grep "iPhone 15 (" | \
                  grep -oE '\([A-Z0-9-]+\)' | \
                  tr -d '()')
                
                xcrun simctl boot $DEVICE_ID
                
                # Wait for boot
                xcrun simctl bootstatus $DEVICE_ID

          - task: Bash@3
            displayName: 'Install App on Simulator'
            inputs:
              targetType: 'inline'
              script: |
                DEVICE_ID=$(xcrun simctl list devices | \
                  grep "iPhone 15 (" | \
                  grep -oE '\([A-Z0-9-]+\)' | \
                  tr -d '()')
                
                xcrun simctl install $DEVICE_ID \
                  "$(Pipeline.Workspace)/ios-app/*.app"

          - task: DotNetCoreCLI@2
            displayName: 'Run UI Tests'
            inputs:
              command: 'test'
              projects: 'src/MyApp.Tests.UITests/MyApp.Tests.UITests.csproj'
              arguments: '--configuration $(buildConfiguration) --logger trx'
              publishTestResults: true

          - task: PublishTestResults@2
            displayName: 'Publish UI Test Results'
            condition: always()
            inputs:
              testResultsFormat: 'VSTest'
              testResultsFiles: '**/*.trx'
              mergeTestResults: true
              failTaskOnFailedTests: true

          - task: PublishBuildArtifacts@1
            displayName: 'Publish UI Test Screenshots'
            condition: always()
            inputs:
              pathToPublish: 'src/MyApp.Tests.UITests/screenshots'
              artifactName: 'ui-test-screenshots'

Quality Gates

I configure quality gates that must pass before deployment:

# In api-pipeline.yml, add quality gate stage

  - stage: QualityGate
    displayName: 'Quality Gate Validation'
    dependsOn: Build
    jobs:
      - job: ValidateQuality
        displayName: 'Check Quality Metrics'
        steps:
          - task: BuildQualityChecks@9
            displayName: 'Check Code Coverage'
            inputs:
              checkCoverage: true
              coverageFailOption: 'fixed'
              coverageThreshold: '80'
              coverageType: 'lines'

          - task: BuildQualityChecks@9
            displayName: 'Check Test Pass Rate'
            inputs:
              checkWarnings: true
              warningFailOption: 'fixed'
              warningThreshold: '0'
              
          - task: Bash@3
            displayName: 'Run Static Code Analysis'
            inputs:
              targetType: 'inline'
              script: |
                # Install security scanner
                dotnet tool install --global security-scan
                
                # Run security scan
                security-scan src/**/*.csproj \
                  --excl-proj=**/*Tests*.csproj
                  
                # Check for vulnerabilities
                if [ $? -ne 0 ]; then
                  echo "Security vulnerabilities detected!"
                  exit 1
                fi

Part 6: Deployment Strategies and Monitoring

Blue-Green Deployment for the API

For zero-downtime API deployments, I use Azure App Service deployment slots:

# Enhanced deployment stage with slots

  - stage: DeployProdWithSlots
    displayName: 'Deploy to Production (Blue-Green)'
    dependsOn:
      - Build
      - QualityGate
    condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
    jobs:
      - deployment: DeployToStaging
        displayName: 'Deploy to Staging Slot'
        environment: 'Production'
        strategy:
          runOnce:
            deploy:
              steps:
                - task: AzureWebApp@1
                  displayName: 'Deploy to Staging Slot'
                  inputs:
                    azureSubscription: 'Azure-Service-Connection'
                    appName: 'myapp-api-prod'
                    package: '$(Pipeline.Workspace)/drop/api/*.zip'
                    deployToSlotOrASE: true
                    resourceGroupName: 'myapp-prod-rg'
                    slotName: 'staging'

                - task: AzureAppServiceManage@0
                  displayName: 'Start Staging Slot'
                  inputs:
                    azureSubscription: 'Azure-Service-Connection'
                    Action: 'Start Azure App Service'
                    WebAppName: 'myapp-api-prod'
                    SpecifySlotOrASE: true
                    ResourceGroupName: 'myapp-prod-rg'
                    Slot: 'staging'

                - task: Bash@3
                  displayName: 'Health Check Staging Slot'
                  inputs:
                    targetType: 'inline'
                    script: |
                      echo "Waiting for app to warm up..."
                      sleep 30
                      
                      # Health check with retries
                      for i in {1..5}; do
                        STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
                          https://myapp-api-prod-staging.azurewebsites.net/health)
                        
                        if [ $STATUS -eq 200 ]; then
                          echo "Health check passed!"
                          exit 0
                        fi
                        
                        echo "Attempt $i failed (Status: $STATUS). Retrying..."
                        sleep 10
                      done
                      
                      echo "Health check failed after 5 attempts!"
                      exit 1

                - task: Bash@3
                  displayName: 'Run Smoke Tests'
                  inputs:
                    targetType: 'inline'
                    script: |
                      # Run critical smoke tests against staging slot
                      dotnet test src/MyApp.Tests.Smoke/MyApp.Tests.Smoke.csproj \
                        --configuration Release \
                        --logger "trx" \
                        -- TestRunParameters.Parameter(name="BaseUrl", \
                           value="https://myapp-api-prod-staging.azurewebsites.net")

                - task: AzureAppServiceManage@0
                  displayName: 'Swap Staging to Production'
                  inputs:
                    azureSubscription: 'Azure-Service-Connection'
                    Action: 'Swap Slots'
                    WebAppName: 'myapp-api-prod'
                    ResourceGroupName: 'myapp-prod-rg'
                    SourceSlot: 'staging'
                    SwapWithProduction: true

                - task: Bash@3
                  displayName: 'Verify Production Deployment'
                  inputs:
                    targetType: 'inline'
                    script: |
                      echo "Verifying production deployment..."
                      sleep 10
                      
                      STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
                        https://myapp-api-prod.azurewebsites.net/health)
                      
                      if [ $STATUS -eq 200 ]; then
                        echo "Production deployment verified!"
                      else
                        echo "Production health check failed! Rolling back..."
                        # Swap back to previous version
                        exit 1
                      fi

Monitoring and Observability

Post-deployment monitoring is essential. I integrate Application Insights with custom telemetry:

// Program.cs - Add comprehensive monitoring

builder.Services.AddApplicationInsightsTelemetry(options =>
{
    options.ConnectionString = builder.Configuration["ApplicationInsights:ConnectionString"];
    options.EnableAdaptiveSampling = false; // Full telemetry in production
});

builder.Services.AddSingleton();

// Custom telemetry initialization
public class CustomTelemetryInitializer : ITelemetryInitializer
{
    public void Initialize(ITelemetry telemetry)
    {
        telemetry.Context.Cloud.RoleName = "MyApp.Api";
        telemetry.Context.GlobalProperties["Environment"] = 
            Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Unknown";
        telemetry.Context.GlobalProperties["Version"] = 
            Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "Unknown";
    }
}

// Custom metrics tracking
public class MetricsService
{
    private readonly TelemetryClient _telemetry;

    public MetricsService(TelemetryClient telemetry)
    {
        _telemetry = telemetry;
    }

    public void TrackSyncOperation(string userId, SyncResult result)
    {
        _telemetry.TrackEvent("SyncCompleted", new Dictionary
        {
            { "UserId", userId },
            { "ItemsCreated", result.Created.ToString() },
            { "ItemsUpdated", result.Updated.ToString() },
            { "ItemsDeleted", result.Deleted.ToString() },
            { "Conflicts", result.Conflicts.ToString() },
            { "Errors", result.Errors.ToString() }
        });
        
        _telemetry.TrackMetric("Sync.ItemsProcessed", 
            result.Created + result.Updated + result.Deleted);
        _telemetry.TrackMetric("Sync.Conflicts", result.Conflicts);
        _telemetry.TrackMetric("Sync.Errors", result.Errors);
    }
}

Key Learnings and Best Practices

After building numerous applications with this workflow, here are the most valuable insights I've gained:

Working Effectively with Claude Code

  • Be Specific with Context: The more context you provide about your architecture and requirements, the better the generated code will be. I often share existing code patterns as examples.
  • Iterate Incrementally: Don't try to generate an entire feature at once. Build in layers—start with models, then services, then ViewModels, then UI.
  • Review Everything: AI-generated code is a starting point, not the finish line. Always review for security issues, performance concerns, and consistency with your codebase.
  • Learn from the Output: Claude Code often introduces patterns or APIs I wasn't aware of. I treat it as a learning tool, not just a code generator.

Pipeline Best Practices

  • Fast Feedback Loops: Keep the build pipeline fast. Run unit tests first, integration tests next, and expensive UI tests only for critical paths or on-demand.
  • Parallel Execution: Run independent stages in parallel (API build and iOS build can happen simultaneously).
  • Comprehensive Logging: Add detailed logging to pipeline steps. When deployments fail at 2 AM, good logs are invaluable.
  • Secrets Management: Never commit secrets. Use Azure Key Vault with managed identities wherever possible.
  • Rollback Strategy: Always have a rollback plan. Deployment slots for APIs and versioned releases for mobile apps provide safety nets.

TestFlight Distribution Tips

  • Automate Everything: Manual uploads to App Store Connect waste time and introduce errors. Full automation from build to tester notification is worth the initial setup effort.
  • Version Numbering: Use semantic versioning with build numbers from CI. This makes it easy to trace a TestFlight build back to source code.
  • Release Notes: Automatically generate release notes from commit messages or pull request titles. Testers appreciate knowing what changed.
  • Testing Groups: Maintain separate groups (internal team, trusted testers, broader beta) and promote builds through these stages.

Challenges and Solutions

Challenge: iOS builds taking 20+ minutes in the pipeline.
Solution: Implemented caching of NuGet packages and iOS SDK components. Reduced build time to 8-10 minutes.

Challenge: Integration tests failing intermittently in CI but passing locally.
Solution: Database state wasn't being properly reset between tests. Added test fixtures that ensure clean state for each test run.

Challenge: TestFlight uploads timing out during peak hours.
Solution: Implemented retry logic with exponential backoff. Also scheduled automated builds during off-peak hours when possible.

Challenge: Claude Code generating code that doesn't compile due to outdated API knowledge.
Solution: Provide current documentation snippets when asking for help with newer APIs. Also, review generated code against official docs for newer frameworks.

Conclusion: The Future of Development Workflows

Building complete solutions—from mobile app to API to automated deployment—used to take weeks of setup before writing the first line of business logic. With Claude Code as an AI development partner and modern DevOps practices in Azure, I can now have a full-stack project with working CI/CD pipelines running in days, not weeks.

This isn't about replacing developers—it's about augmenting our capabilities. Claude Code handles the boilerplate, suggests best practices, and accelerates implementation. I still make all the architectural decisions, review every line of code, and ensure quality meets professional standards. But I'm dramatically more productive because I'm spending time on the problems that matter, not fighting with configuration files or remembering obscure API syntax.

Key Takeaways

  • AI-Assisted Development Accelerates Without Sacrificing Quality: With proper review processes, Claude Code generates production-ready code that follows best practices.
  • Comprehensive CI/CD Is Non-Negotiable: Automated testing and deployment pipelines catch issues early and enable confident, frequent releases.
  • Azure DevOps Excels for .NET Stacks: The deep integration with Microsoft technologies makes Azure DevOps the natural choice for .NET MAUI and ASP.NET Core projects.
  • TestFlight Automation Streamlines Mobile Distribution: Eliminating manual upload steps ensures consistent, fast delivery to testers.
  • Monitoring Completes the Loop: Application Insights and custom telemetry provide visibility into how applications perform in production.

Looking Forward

This workflow continues to evolve. I'm currently exploring:

  • GitHub Copilot Workspace integration for even tighter AI assistance during development
  • Automated mobile UI testing using Appium in the pipeline
  • Container-based deployments for the API using Azure Container Apps
  • Infrastructure as Code with Bicep for complete environment reproducibility

The combination of .NET MAUI for cross-platform mobile development, ASP.NET Core for robust APIs, Azure DevOps for comprehensive CI/CD, and Claude Code for AI-powered development assistance has become my standard approach for building professional applications efficiently. Whether you're a solo developer like me at MallitLabs or part of a larger team, these patterns and practices provide a solid foundation for modern application development.

If you're considering adopting AI-assisted development or modernizing your DevOps pipelines, I hope this detailed walkthrough of my real-world workflow provides practical guidance and inspiration. The future of software development is here, and it's about humans and AI working together to build better software, faster.

Happy coding, and may your pipelines always be green!

Ready to start your project?

Let's discuss how MallitLabs can help bring your vision to life.