Skip to content

Instantly share code, notes, and snippets.

@Lamparter
Last active October 7, 2024 20:24
Show Gist options
  • Save Lamparter/7c172eb9d36b9dfcaffb86b670e239f4 to your computer and use it in GitHub Desktop.
Save Lamparter/7c172eb9d36b9dfcaffb86b670e239f4 to your computer and use it in GitHub Desktop.
Threads API wrapper for .NET response handler examples

🧵 ThreadSharp API Wrapper spec

This document contains information regarding speculative implementations for a .NET wrapper for the Threads API. This collection of documents primarily answers the following question:

How would you handle getting responses from a web API in a strongly typed programming language where you can specify which fields you want to retrieve? And why?

1️⃣ Get all the possible fields & return it.

2️⃣ Make all the optional fields nullable in the model, and allow the library consumer to decide which fields they want to be non-null.

3️⃣ Return the JSON nodes directly (depends on the JSON lib)

4️⃣ Separate partial from full, both library & model side.

📝 Proposal documentation

Documentation for proposals and explanation including pros and cons

HybridApproach-Proposal.cs

This approach aims to provide the flexibility of querying specific fields while maintaining some level of type safety and readability.

  1. Query API with Specific Fields: When making an API request, specify the fields you need. For example:
{
        "query": ["username", "email"]
}
  1. Dynamic Model Generation: Rather than defining a static model for each possible API response, dynamically generate a model based on the requested fields.

  2. Use Strong Typing with Optional Fields: Define a base model with common fields and extend it with optional fields dynamically. This can be achieved using a combination of dictionaries and generic methods to provide type safety without sacrificing flexibility.

Benefits

  • Flexibility: Easily adapt to changes in the API by querying only the required fields.
  • Type Safety: Use generic methods to provide some level of type checking.
  • Readability: More readable than raw dictionaries, as you can abstract field access logic.

Comparisons

Pro Con
vs. Dictionary Approach Adds type safety and readability Slightly more complex than a raw dictionary but more maintainable.
vs. Nullable Fields Model Approach More flexible and less verbose, especially with many optional fields. Slightly less type-safe but balances flexibility and type safety better.
// Draft proposal
public class ApiResponse
{
public Dictionary<string, object> Fields { get; set; }
}
public interface IUserAdapter
{
void Adapt(ApiResponse response);
}
public class UserMinimalAdapter : IUserAdapter
{
public string Username { get; set; }
public string Email { get; set; }
public void Adapt(ApiResponse response)
{
Username = response.Fields["username"].ToString();
Email = response.Fields["email"].ToString();
}
}
public class UserDetailedAdapter : UserMinimalAdapter
{
public string FullName { get; set; }
public DateTime? LastLogin { get; set; }
public new void Adapt(ApiResponse response)
{
base.Adapt(response);
FullName = response.Fields["fullname"]?.ToString();
LastLogin = response.Fields.ContainsKey("lastlogin") ? (DateTime?)response.Fields["lastlogin"] : null;
}
}

rokx

Make it a dict explicitly...? I bet the API would be like query: ["username", "email"] anyways. And then the result dict would contains corresponding keys Make the keys typed to reduce typo and thats about it

using System;
using System.Collections.Generic;
using System.Text.Json;

public class ApiResponseHandler
{
    public Dictionary<string, object> Fields { get; set; }

    public ApiResponseHandler(string jsonResponse)
    {
        Fields = JsonSerializer.Deserialize<Dictionary<string, object>>(jsonResponse);
    }

    public T GetField<T>(string fieldName)
    {
        if (Fields.ContainsKey(fieldName) && Fields[fieldName] is JsonElement element)
        {
            return JsonSerializer.Deserialize<T>(element.GetRawText());
        }
        return default;
    }
}

// Usage example
public class Program
{
    public static void Main()
    {
        // Simulate an API response
        string jsonResponse = "{ \"username\": \"johndoe\", \"email\": \"[email protected]\" }";

        // Initialize the response handler
        ApiResponseHandler responseHandler = new ApiResponseHandler(jsonResponse);

        // Extract fields
        string username = responseHandler.GetField<string>("username");
        string email = responseHandler.GetField<string>("email");

        // Output extracted fields
        Console.WriteLine($"Username: {username}");
        Console.WriteLine($"Email: {email}");
    }
}

This example was written by Lamparter

public class User
{
public string Username { get; set; }
public string Sanity { get; set; }
public string WhatIsThis { get; set; }
public DateTime? HelpMe { get; set; }
}
public class UserSerializer
{
public static string Serialize(User user, string[] fields)
{
var result = new Dictionary<string, object>();
if (fields.Contains("username"))
result["username"] = user.Username;
if (fields.Contains("sanity"))
result["sanity"] = user.Sanity;
if (fields.Contains("whatisthis"))
result["whatisthis"] = user.WhatIsThis;
if (fields.Contains("helpme"))
result["helpme"] = user.HelpMe;
return JsonConvert.SerializeObject(result);
}
}
// Draft proposal
public interface IUser { }
public class UserMinimal : IUser
{
public string Username { get; set; }
public string Email { get; set; }
}
public class UserDetailed : UserMinimal
{
public string FullName { get; set; }
public DateTime? LastLogin { get; set; }
}
public static class UserFactory
{
public static IUser CreateUser(string[] fields)
{
if (fields.Contains("fullname") || fields.Contains("lastlogin"))
{
return new UserDetailed();
}
return new UserMinimal();
}
}
public class ApiResponse
{
public Dictionary<string, object> Fields { get; set; }
public T GetField<T>(string fieldName)
{
if (Fields.ContainsKey(fieldName) && Fields[fieldName] is T value)
{
return value;
}
return default;
}
}
// Usage:
var response = QueryApi(new[] { "username", "email" });
var apiResponse = JsonConvert.DeserializeObject<ApiResponse>(response);
string username = apiResponse.GetField<string>("username");
string email = apiResponse.GetField<string>("email");
// Draft proposal
public class User
{
public string Username { get; set; }
public string Email { get; set; }
public string FullName { get; set; } // Optional
public DateTime? LastLogin { get; set; } // Optional
}
// Usage:
var response = QueryApi(new[] { "username", "email" });
var user = JsonConvert.DeserializeObject<User>(response);
// Draft proposal
public class UserMinimal
{
public string Username { get; set; }
public string Email { get; set; }
}
public class UserDetailed : UserMinimal
{
public string FullName { get; set; }
public DateTime? LastLogin { get; set; }
}
// Usage:
var response = QueryApi(new[] { "username", "email" });
var user = JsonConvert.DeserializeObject<UserMinimal>(response);
@Lamparter
Copy link
Author

@itsWindows11 here are a few proposals for response handlers. Enjoy! 😄

@Lamparter
Copy link
Author

I've included explanation and docs for the hybrid approach (the one that wouldn't work with AOT) but if you want me to explain something else I can add it!

@itsWindows11
Copy link

itsWindows11 commented Sep 3, 2024

Few notes:

  1. The library will be called ThreadSharp, not Threads.NET due to the threads.net domain actually belonging to Threads which might cause confusion.
  2. I was gonna end up doing something similar to your first example under roxk's suggestion.

So the end result basically will be: we use objects with nullable fields in API wrapper methods. A separate method on the client level will be created so the library consumer will be able to call the API with a custom endpoint and specify their own fields, and they get a Dictionary<string, JsonElement> as a response. This helps futureproof the library as well as preserve type safety in API wrapper methods, and library consumers can use either approach to call the Threads API.

@itsWindows11
Copy link

With the other approaches there's too much complexity IMO, sounds like an idea for a separate convenience library to handle optional stuff.

@Lamparter
Copy link
Author

Yeah you're right - these were just a few experimental proposals

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment