AceLand Unity Packages
  • Home
  • Getting Started
    • Installation
    • Project Settings
    • Architecture Graph
    • Development Level
  • Tutorial
    • Create Your Package
    • Create Project Settings
  • Packages
    • Library
      • Change Log
      • Editor Tools
      • Mono
        • Follow Object
        • Singleton
      • Attributes
        • Conditional Show
        • Inspector Button
        • ReadOnly Field
      • Build Leveling
      • CVS
      • DataTools
      • Disposable Object
      • Extensions
      • Json
      • Kalman Filter
      • Optional
      • Project Setting
      • Serialization Surrogate
      • Utils
    • Event Driven
      • Change Log
      • Event Bus
      • Signal
    • Input
      • Change Log
    • Memento Service
      • Change Log
    • Node Framework
      • Change Log
      • Mono Node
    • Node For Mono (dev)
    • Player Loop Hack
      • Change Log
    • Pool
      • Change Log
    • Security (dev)
    • States
      • Change Log
    • Task Utils
      • Change Log
    • WebRequest
      • Change Log
Powered by GitBook
On this page
  • In One Line
  • Overview
  • Package Info
  • Why Use It?
  • Key Features
  • Project Settings
  • Usage
  • Sample of Weather Widget
  • Advanced Sample
  1. Packages

WebRequest

A robust HTTP client solution for Unity, offering flexible API management, automatic retries, and comprehensive request handling using .NET's HttpClient.

In One Line

REST assured, your requests are in good hands!

Overview

AceLand WebRequest is a modern web request solution designed specifically for Unity projects, providing a fluent interface for API calls with built-in error handling, retry mechanisms, and API section management. It uses .NET's HttpClient instead of UnityWebRequest for better performance and more control.

Package Info

display name

AceLand WebRequest

package name

latest version

1.0.10

namespace

git repository

dependencies

Why Use It?

  • Modern Architecture: Built on .NET's HttpClient for better performance and reliability

  • Flexible API Management: Test and switch between different API configurations in editor

  • Built-in Retry Logic: Automatic retry mechanism with configurable intervals

  • Type-Safe Builder Pattern: Fluent interface for building requests with compile-time safety

  • Comprehensive Error Handling: Detailed error logging and exception management

  • Multiple Content Types: Support for JSON, Form, and Multipart request types

Key Features

  • Custom retry intervals and timeout settings

  • Comprehensive logging system with different build levels

  • HTTPS enforcement option

  • Automatic timestamp header injection

  • JSON validation before sending

  • Support for cancellation tokens

  • Editor tools for API section management


Project Settings

Logging

---

Logging Level

Level of Logging on web request default: BuildLevel.Production

Result Logging Level

Level of Logging on request success

default: BuildLevel.DevelopmentBuild

Checking Options

---

Check Json Before Send

Check given content is a valid json before send request. This may be expensive if big content. Default: false

Force Https Scheme

Require request url is https if true. Otherwise http will also ok. Default: true

Header Auto Fill

---

Add Time In Header

Add send time in header automatically. Default: true

Time Key

key of Time in header. default: Time

Auto Fill Headers

Add default headers on each Request. Same headers will be covered by WithHeaders options on building Request. Default: { "User-Agent", "Mozilla/5.0" }

Request Options

---

Request Timeout

Default timeout if not set in request handle. Default: 3000 ms

Long Request Timeout

Default long request timeout. It will be set if building request handle with long request. Default: 15000 ms

Request Retry

How many times will retry on connection error including timeout. Default: 3

Retry Interval

Interval between each retry. Default: [ 400, 800, 1600, 3200, 6400, 12800, 25600] in ms

Current API Section

---

Section

[Lock] Current API section.

Domain

[Lock] Current API Domain.

Version

[Lock] Current API Version.

- button

Clear Current API Section Data.

Api Url

[Lock] API URL of current section.

API Section Editor

---

Table

This is API Sections Data. All data is saved in Assets/Editor/AceLand/api_sections.asset that confirms non-used data will not be built.

Last line is an empty line for adding new data. - delete data * apply to Current API Section + add data Save save to asset Restore restore unsaved changes

You can get data of Current API Section by Request.ApiUrl, etc.


Usage

// Create a GET Request Handler
IRequestHandle request = Request.Get()
    .WithUrl("https://api.example.com/data".ToUri())
    .WithHeader("User-Agent", "MyUnityApp/1.0")
    .Build();
    
// Create a POST Request Handler
IRequestHandle request = Request.Post()
    .WithUrl("https://api.example.com/users".ToUri())
    .WithJsonContent()
    .WithContent("{\"name\": \"John\"}")
    .WithHeader("Authorization", "Bearer token")
    .WithTimeout(7500)
    .Build();
    
// Create a POST Request Handler with Multipart form data with file
IRequestHandle request = Request.Post()
    .WithUrl("https://api.example.com/upload".ToUri())
    .WithMultipartContent()
    .WithStreamData("file", "path/to/file.jpg", "image.jpg")
    .WithContent("description", "Profile photo")
    .Build();
    
// Send the request and get reponse
JToken response = await request.Send();
    
// Send the request and receive specified type of data
YourData response = await request.Send<YourData>();

// Cancel the request
request.Cancel();

// Dispose the request
request.Disposal();

// Get result from finished request
JToken result = request.Result;

Sample of Weather Widget

For example, there is a Weather Widget on canvas. It will refresh every set time.

using System.Threading;
using System.Threading.Tasks;
using AceLand.TaskUtils;
using AceLand.WebRequest;
using TMPro;
using UnityEngine;
using UnityEngine.UI;

public class TestWeatherWidget : MonoBehaviourr
{
    [Header("Weather Widget")]
    // WeatherProfile contains sprite of weather state
    [SerializeField] private WeatherProfile profile;
    // HongKongLocations contains districts and coordinates
    [SerializeField] private HongKongLocations location = HongKongLocations.CentralAndWestern;
    [SerializeField] private int updateInterval = 180;
    [SerializeField] private Image weatherIcon;
    [SerializeField] private TextMeshProUGUI tempLabel;
    [SerializeField] private TextMeshProUGUI apparentTempLabel;
    
    // setup API
    private const string LATITUDE_KEY = "{latitude}";
    private const string LONGITUDE_KEY = "{longitude}";
    private const string API = "https://api.open-meteo.com/v1/" +
                               "forecast?latitude=" + LATITUDE_KEY +
                               "&longitude=" + LONGITUDE_KEY +
                               "&current=temperature_2m,weather_code,rain,cloud_cover,showers,apparent_temperature,is_day" +
                               "&timezone=auto" +
                               "&forecast_days=1";

    private CancellationTokenSource _tokenSource;
    
    private void Start()
    {
        _tokenSource = new CancellationTokenSource();
        // Linked with Application Alive Token
        var token = Promise.LinkedOrApplicationAliveToken(_tokenSource, out _);
        RequestWebData(token);
    }

    private void OnDisable()
    {
        if (!_tokenSource.IsCancellationRequested)
            _tokenSource?.Cancel();
        _tokenSource?.Dispose();
    }
    
    private void UpdateWeather(WeatherWidgetData data)
    {
        weatherIcon.sprite = profile.GetIcon(data.WeatherCode, data.IsDay);
        tempLabel.text = data.Temperature;
        apparentTempLabel.text = $"Apparent: {data.ApparentTemperature}";
    }

    private void RequestWebData(CancellationToken token)
    {
        if (token.IsCancellationRequested) return;

        var (latitude, longitude) = location.ToGeoData();
        Uri api = API.Replace(LATITUDE_KEY, latitude.ToString("F4"))
            .Replace(LONGITUDE_KEY, longitude.ToString("F4"))
            .ToUri();
        int intervalTime = updateInterval * 1000;
        
        // create request
        IRequestHandle request = Request.Get()
            .WithUrl(api)
            .Build();

        // send request
        request.Send<WeatherWebData>()
            .Then(result =>
            {
                // convert data
                WeatherWidgetData widgetData = result.ToWidgetData(location);
                // use data
                UpdateWeather(widgetData);
                // prepare next call
                WaitForRefresh(intervalTime, token);
            })
            .Catch(exception =>
            {
                // prepare next call
                WaitForRefresh(intervalTime, token);
            })
            .Final(() =>
            {
                // dispose current request
                request.Dispose();
            });
    }

    private void WaitForRefresh(int intervalTime, CancellationToken token)
    {
        if (token.IsCancellationRequested) return;
        // wait for next call
        Task.Delay(intervalTime, token)
            .Then(() => RequestWebData(token));
    }
}

Work with Promise Awaiter for thread-safe and clear coding structure.


Advanced Sample

This is a sample of building a User Authorization API serivce and how to use it.

Following library and packages are using in above example:

using System.Threading.Tasks;
using AceLand.WebRequest;

// create a base class containing main urls
public abstract class BaseAPI
{
    // reference to Api Url in Project Setting
    private protected static string APIUrl => Request.ApiUrl;
}

// create UserAPI Service
public sealed class UserAPI : BaseAPI
{
    // prepare all api urls.
    private const string BASIC_AUTH_URL = "/basic-auth/{user}/{passwd}";
    
    private static IRequestHandle _request;
    
    // create a Promise to disposal request handle on finished
    public static Promise<UserData> BasicAuth(string user, string password)
    {
        // combine a complete URL
        string url = APIUrl + BASIC_AUTH_URL
            .Replace("{user}", user)
            .Replace("{passwd}", password);
        
        // build Request Handle
        _request = Request.Get()
            .WithUrl(url.ToUri())
            .Build();
        
        // await response with specified type
        return _request.Send<UserData>()
            .Final(_request.Disposal);
    }
    
    public static async CancelBasicAuth()
    {
        _request?.Cancel();
    }
}
using AceLand.Library.Optional;

// Event for User Login
public interface IUserLoginEvent { }
public interface IUserCancelLoginEvent { }
public readonly struct UserLoginData : IValidatable
{
    public readonly string User;
    public readonly string Password;
    private readonly UserLoginDataValidator _validator;

    // use Optional for null handling
    public UserLoginData(Option<string> user, Option<string> password)
    {
        User = user.Map(t => t.Trim()).Reduce(() => string.Empty);
        Password = password.Map(t => t.Trim()).Reduce(() => string.Empty);
        _validator = new UserLoginDataValidator();
    }

    // Validate User Login Data
    public bool Validate() => _validator.Validate(this);
}

// Event for User Authorization Service
public interface IUserAuthEvent { }
public enum UserAuthState
{
    None, Authorized, WrongUserPasswd, AuthError
}
using AceLand.Library.Extensions;

public interface IValidatable
{
    bool Validate();
}

public abstract class Validator<T> where T : IValidatable
{
    public virtual bool Validate(T data) => false;
}

public sealed class UserLoginDataValidator : Validator<UserLoginData>
{
    private const int USERNAME_MIN_LENGTH = 8;
    private const int PASSWORD_MIN_LENGTH = 8;

    public override bool Validate(UserLoginData data) =>
        !data.User.IsNullOrEmptyOrWhiteSpace() &&
        !data.Password.IsNullOrEmptyOrWhiteSpace() &&
        data.User.Length > USERNAME_MIN_LENGTH &&
        data.Password.Length > PASSWORD_MIN_LENGTH;
}
using System;
using AceLand.EventDriven.Bus;
using AceLand.Library.Mono;
using AceLand.TaskUtils;

public class UserAuthorization : Singleton<UserAuthorization>
{
    private UserAuthState _userAuthState;
    private UserData _userData;
    private bool _busy;
    
    private void OnEnable()
    {
        // Listen to events
        EventBus.Event<IUserLoginEvent>()
            .WithListener<UserLoginData>(OnUserLogin)
            .WithKickStart()
            .Listen();
            
        EventBus.Event<IUserCancelLoginEvent >()
            .WithListener(OnUserCancelLogin)
            .WithKickStart()
            .Listen();
    }
    
    private void OnDisable()
    {
        // Unlisten from IUserLoginEvent
        EventBus.Event<IUserLoginEvent>()
            .Unlisten<UserLoginData>(OnUserLogin);
            
        EventBus.Event<IUserCancelLoginEvent >()
            .Unlisten(OnUserCancelLogin);
    }

    private void OnUserLogin(object sender, UserLoginData data)
    {
        if (sender is not UiLoginHandle)
            throw new Exception($"Illegal User Auth Sender detected: {typeof(sender)}");
    
        // checking policies
        if (!data.Validate())
        {
            OnAuthEvent(UserAuthState.WrongUserPasswd);
            return;
        }
        
        // start user auth
        HandleUserAuth(data);
    }
    
    private void OnUserCancelLogin(object sender)
    {
        if (sender is not UiLoginHandle)
            throw new Exception($"Illegal User Auth Sender detected: {typeof(sender)}");
        
        // cancel user auth
        CancelUserAuth();
    }

    private void HandleUserAuth(UserLoginData data)
    {
        if (_busy) return;
        if (_userAuthState is UserAuthState.Authorized)
        {
            OnAuthSuccess(_userData);
            return;
        }
        
        _busy = true;
        
        // Use Promise Awaiter to handle request
        UserAPI.BasicAuth(data.User, data.Password)
            .Then(OnAuthSuccess)
            .Catch(_ => OnAuthError(UserAuthState.AuthError))
            .Final(OnAuthFinish);
    }
    
    private void CancelUserAuth()
    {
        if (!_busy) return;
        UserAPI.CancelBasicAuth();
    }

    private void OnAuthSuccess(UserData userData)
    {
        _userData = userData;
        OnAuthEvent(UserAuthState.Authorized);
    }

    private void OnAuthEvent(UserAuthState state)
    {
        _userAuthState = state;
        // raise event IUserAuthEvent
        EventBus.Event<IUserAuthEvent>()
            .WithSender(this)
            .WithData(state)
            .Raise();
    }

    private void OnAuthFinish()
    {
        _busy = false;
    }
}
using AceLand.Library.Optional;
using UnityEngine;

public class UiLoginHandle : MonoBehaviour
{
    // use Optional for null handling
    private Option<string> _username;
    private Option<string> _password;

    // link to Input Component
    public void OnUsernameInput(string value) =>
        _username = value.ToOption();
    
    // link to Input Component    
    public void OnPasswordInput(string value) =>
        _password = value.ToOption();

    // link to Button Component
    public void OnLoginButtonPressed()
    {
        // create data
        UserLoginData data = new UserLoginData(_username, _password);
        
        // raize event
        EventBus.Event<IUserLoginEvent>()
            .WithSender(this)
            .WithData(data)
            .Raise();
    }
    
    public void OnCancelButtonPressed()
    {
        _username = string.Empty;
        _password = string.Empty;
        
        // raize event
        EventBus.Event<IUserCancelLoginEvent>()
            .WithSender(this)
            .WithData(data)
            .Raise();
    }
}

Last updated 5 days ago

Please read for details.

com.aceland.webrequest
AceLand.WebRequest
com.aceland.library: 1.0.18
com.aceland.taskutils: 1.0.6
com.unity.nuget.newtonsoft-json: 3.2.1
Task Utils
Library.Mono
Optional
Singleton
Event Driven
EventBus
Task Utils
https://github.com/parsue/com.aceland.webrequest.git