Technology

C# MAUI Load Testing Strategy

c maui load testing strategy
C# MAUI Load Testing Strategy | SiamCafe Blog
2025-09-27· อ. บอม — SiamCafe.net· 11,277 คำ

.NET MAUI Cross-platform Development

.NET MAUI เป็น Framework สร้าง Cross-platform Apps ด้วย C# จาก Codebase เดียว รันบน Android, iOS, macOS และ Windows ใช้ MVVM Pattern แยก UI ออกจาก Business Logic ทดสอบได้ง่าย

Load Testing เป็นส่วนสำคัญก่อน Release App ทดสอบว่า Backend API รองรับผู้ใช้จำนวนมากได้ วัด Response Time, Throughput หา Bottleneck แก้ไขก่อน Go-live

.NET MAUI App Development

// === .NET MAUI App ด้วย MVVM Pattern ===
// dotnet new maui -n MyApp
// cd MyApp

// === Models/User.cs ===
namespace MyApp.Models;

public class User
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public string Email { get; set; } = string.Empty;
    public string Avatar { get; set; } = string.Empty;
    public DateTime CreatedAt { get; set; }
}

public class ApiResponse<T>
{
    public bool Success { get; set; }
    public T? Data { get; set; }
    public string Message { get; set; } = string.Empty;
    public int TotalCount { get; set; }
}

// === Services/ApiService.cs ===
namespace MyApp.Services;

public interface IApiService
{
    Task<ApiResponse<List<User>>> GetUsersAsync(int page = 1, int limit = 20);
    Task<ApiResponse<User>> GetUserAsync(int id);
    Task<ApiResponse<User>> CreateUserAsync(User user);
}

public class ApiService : IApiService
{
    private readonly HttpClient _client;
    private const string BaseUrl = "https://api.example.com/v1";

    public ApiService()
    {
        _client = new HttpClient
        {
            BaseAddress = new Uri(BaseUrl),
            Timeout = TimeSpan.FromSeconds(30),
        };
    }

    public async Task<ApiResponse<List<User>>> GetUsersAsync(int page = 1, int limit = 20)
    {
        var response = await _client.GetAsync($"/users?page={page}&limit={limit}");
        response.EnsureSuccessStatusCode();
        var json = await response.Content.ReadAsStringAsync();
        return System.Text.Json.JsonSerializer.Deserialize<ApiResponse<List<User>>>(json)!;
    }

    public async Task<ApiResponse<User>> GetUserAsync(int id)
    {
        var response = await _client.GetAsync($"/users/{id}");
        response.EnsureSuccessStatusCode();
        var json = await response.Content.ReadAsStringAsync();
        return System.Text.Json.JsonSerializer.Deserialize<ApiResponse<User>>(json)!;
    }

    public async Task<ApiResponse<User>> CreateUserAsync(User user)
    {
        var content = new StringContent(
            System.Text.Json.JsonSerializer.Serialize(user),
            System.Text.Encoding.UTF8, "application/json");
        var response = await _client.PostAsync("/users", content);
        response.EnsureSuccessStatusCode();
        var json = await response.Content.ReadAsStringAsync();
        return System.Text.Json.JsonSerializer.Deserialize<ApiResponse<User>>(json)!;
    }
}

// === ViewModels/UsersViewModel.cs ===
namespace MyApp.ViewModels;

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System.Collections.ObjectModel;

public partial class UsersViewModel : ObservableObject
{
    private readonly IApiService _api;

    [ObservableProperty] private bool _isLoading;
    [ObservableProperty] private string _searchText = "";

    public ObservableCollection<User> Users { get; } = new();

    public UsersViewModel(IApiService api)
    {
        _api = api;
    }

    [RelayCommand]
    private async Task LoadUsersAsync()
    {
        if (IsLoading) return;
        IsLoading = true;

        try
        {
            var result = await _api.GetUsersAsync();
            if (result.Success && result.Data != null)
            {
                Users.Clear();
                foreach (var user in result.Data)
                    Users.Add(user);
            }
        }
        catch (Exception ex)
        {
            await Shell.Current.DisplayAlert("Error", ex.Message, "OK");
        }
        finally
        {
            IsLoading = false;
        }
    }

    [RelayCommand]
    private async Task SearchAsync()
    {
        // Filter users by search text
        var result = await _api.GetUsersAsync();
        if (result.Success && result.Data != null)
        {
            var filtered = result.Data
                .Where(u => u.Name.Contains(SearchText, StringComparison.OrdinalIgnoreCase))
                .ToList();
            Users.Clear();
            foreach (var user in filtered)
                Users.Add(user);
        }
    }
}

Load Testing ด้วย k6

// === k6 Load Testing Script ===
// npm install -g k6
// หรือ brew install k6

// load-test.js — ทดสอบ API Backend ของ MAUI App
import http from 'k6/http';
import { check, sleep, group } from 'k6';
import { Rate, Trend, Counter } from 'k6/metrics';

// Custom Metrics
const errorRate = new Rate('error_rate');
const apiLatency = new Trend('api_latency');
const requestCount = new Counter('request_count');

// Test Configuration
export const options = {
  scenarios: {
    // Scenario 1: Smoke Test (5 users, 1 min)
    smoke: {
      executor: 'constant-vus',
      vus: 5,
      duration: '1m',
      startTime: '0s',
    },
    // Scenario 2: Load Test (100 users, ramp up)
    load: {
      executor: 'ramping-vus',
      startVUs: 0,
      stages: [
        { duration: '2m', target: 50 },   // Ramp up to 50
        { duration: '5m', target: 100 },  // Stay at 100
        { duration: '2m', target: 0 },    // Ramp down
      ],
      startTime: '1m',
    },
    // Scenario 3: Stress Test (500 users)
    stress: {
      executor: 'ramping-vus',
      startVUs: 0,
      stages: [
        { duration: '2m', target: 200 },
        { duration: '3m', target: 500 },
        { duration: '2m', target: 0 },
      ],
      startTime: '10m',
    },
  },
  thresholds: {
    http_req_duration: ['p(95)<500', 'p(99)<1000'],
    error_rate: ['rate<0.05'],         // Error rate < 5%
    http_req_failed: ['rate<0.05'],
  },
};

const BASE_URL = __ENV.API_URL || 'https://api.example.com/v1';

export default function () {
  group('GET /users', () => {
    const res = http.get(`/users?page=1&limit=20`, {
      headers: { 'Content-Type': 'application/json' },
    });

    check(res, {
      'status is 200': (r) => r.status === 200,
      'response time < 500ms': (r) => r.timings.duration < 500,
      'has data': (r) => JSON.parse(r.body).data.length > 0,
    });

    errorRate.add(res.status !== 200);
    apiLatency.add(res.timings.duration);
    requestCount.add(1);
  });

  group('GET /users/:id', () => {
    const userId = Math.floor(Math.random() * 100) + 1;
    const res = http.get(`/users/`);

    check(res, {
      'status is 200': (r) => r.status === 200,
      'response time < 300ms': (r) => r.timings.duration < 300,
    });

    errorRate.add(res.status !== 200);
    apiLatency.add(res.timings.duration);
  });

  group('POST /users', () => {
    const payload = JSON.stringify({
      name: `User_`,
      email: `user@test.com`,
    });

    const res = http.post(`/users`, payload, {
      headers: { 'Content-Type': 'application/json' },
    });

    check(res, {
      'status is 201': (r) => r.status === 201,
      'response time < 1000ms': (r) => r.timings.duration < 1000,
    });

    errorRate.add(res.status !== 201);
  });

  sleep(1);
}

// รัน: k6 run load-test.js
// รันกับ Cloud: k6 cloud load-test.js
// รันกับ ENV: k6 run -e API_URL=https://staging.example.com load-test.js

CI/CD Pipeline สำหรับ MAUI + Load Testing

# === GitHub Actions — MAUI Build + Load Test ===
# .github/workflows/maui-loadtest.yml

name: MAUI Build & Load Test
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  build-android:
    runs-on: windows-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-dotnet@v4
        with:
          dotnet-version: 8.0.x

      - name: Install MAUI Workload
        run: dotnet workload install maui-android

      - name: Restore
        run: dotnet restore

      - name: Build Android
        run: |
          dotnet build -c Release -f net8.0-android \
            /p:AndroidKeyStore=true

      - name: Run Unit Tests
        run: dotnet test tests/ --no-build -c Release

  load-test:
    runs-on: ubuntu-latest
    needs: build-android
    steps:
      - uses: actions/checkout@v4

      - name: Start API Server
        run: |
          docker compose -f docker-compose.test.yml up -d
          sleep 10

      - name: Install k6
        run: |
          sudo gpg -k
          sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg \
            --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D68
          echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" | \
            sudo tee /etc/apt/sources.list.d/k6.list
          sudo apt-get update && sudo apt-get install k6

      - name: Smoke Test
        run: k6 run --vus 5 --duration 30s tests/load/smoke.js

      - name: Load Test
        run: k6 run tests/load/load-test.js

      - name: Upload Results
        uses: actions/upload-artifact@v4
        with:
          name: load-test-results
          path: load-test-results.json

      - name: Stop API Server
        run: docker compose -f docker-compose.test.yml down

Best Practices

.NET MAUI คืออะไร

Framework จาก Microsoft สร้าง Cross-platform Apps ด้วย C# XAML รัน Android iOS macOS Windows จาก Codebase เดียว ต่อจาก Xamarin.Forms Performance ดีขึ้น Hot Reload .NET 8+

Load Testing คืออะไร

ทดสอบว่าระบบรองรับผู้ใช้จำนวนมากได้หรือไม่ จำลอง Concurrent Users วัด Response Time Throughput Error Rate หา Bottleneck ใช้ k6 JMeter Locust Artillery

MVVM Pattern คืออะไร

Design Pattern แยก View (UI) ViewModel (Logic) Model (Data) ใช้ Data Binding เชื่อม ทดสอบ Logic ได้ไม่ต้องมี UI เหมาะ MAUI WPF Xamarin

เครื่องมือ Load Testing ตัวไหนดี

k6 JavaScript เร็วใช้ Resource น้อย CI/CD, Locust Python Distributed, JMeter GUI Features มาก, Artillery YAML/JavaScript Microservices เลือกตามภาษาทีมและ Use Case

สรุป

.NET MAUI สร้าง Cross-platform Apps จาก Codebase เดียว ใช้ MVVM Pattern แยก Logic ออกจาก UI Load Testing ด้วย k6 ทดสอบ Backend API ก่อน Release ตั้ง Thresholds สำหรับ P95 Latency Error Rate รัน Load Test ใน CI/CD Pipeline ทุก Release

📖 บทความที่เกี่ยวข้อง

GraphQL Federation Load Testing Strategyอ่านบทความ → Azure Front Door Load Testing Strategyอ่านบทความ → Apache Druid Load Testing Strategyอ่านบทความ → Elasticsearch OpenSearch Load Testing Strategyอ่านบทความ → Neon Serverless Postgres Load Testing Strategyอ่านบทความ →

📚 ดูบทความทั้งหมด →