Skip to content

Instantly share code, notes, and snippets.

@MadhukarMoogala
Created June 17, 2025 08:43
Show Gist options
  • Save MadhukarMoogala/e7ad5ca64d5f3f4f9062d8d7dbfaaaea to your computer and use it in GitHub Desktop.
Save MadhukarMoogala/e7ad5ca64d5f3f4f9062d8d7dbfaaaea to your computer and use it in GitHub Desktop.
Example demonstrating how to extend the archived Forge .NET SDK (forge-api-dotnet-client) to call the BIM 360 Account Admin API from a .NET Framework 4.8 application. This is useful for legacy customers who are still on BIM 360 and cannot yet migrate to Autodesk Construction Cloud (ACC) or .NET 8 SDK.
using Autodesk.Forge;
using Autodesk.Forge.Api;
using Autodesk.Forge.Client;
using RestSharp;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Runtime;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using System.Text.Json;
namespace BIM360.AccountAdmin.App
{
public interface IAccountAdminApi : IApiAccessor
{
Task<ApiResponse<dynamic>> GetProjectsAsyncWithHttpInfo(
string accountId,
string region = null,
string userId = null,
Dictionary<string, string> queryParams = null);
}
public class AccountAdminApi : IAccountAdminApi
{
private Autodesk.Forge.Client.ExceptionFactory _exceptionFactory = (name, response) => null;
public Configuration Configuration { get; set; }
public ExceptionFactory ExceptionFactory
{
get
{
if (_exceptionFactory != null && _exceptionFactory.GetInvocationList().Length > 1)
throw new InvalidOperationException("Multicast delegate for ExceptionFactory is unsupported.");
return _exceptionFactory;
}
set { _exceptionFactory = value; }
}
public AccountAdminApi(Configuration configuration = null)
{
Configuration = configuration ?? Configuration.Default;
ExceptionFactory = Configuration.DefaultExceptionFactory;
}
public string GetBasePath() => Configuration.ApiClient.RestClient.Options.BaseUrl.ToString();
public async Task<ApiResponse<dynamic>> GetProjectsAsyncWithHttpInfo(
string accountId,
string region = null,
string userId = null,
Dictionary<string, string> queryParams = null)
{
if (string.IsNullOrEmpty(accountId))
throw new ApiException(400, "Missing required parameter 'accountId'");
var path = $"/construction/admin/v1/accounts/{accountId}/projects";
var pathParams = new Dictionary<string, string>(); // Do not add "format"
var query = queryParams ?? new Dictionary<string, string>();
var headers = new Dictionary<string, string>(Configuration.DefaultHeader);
var formParams = new Dictionary<string, string>();
var fileParams = new Dictionary<string, FileParameter>();
object postBody = null;
if (!string.IsNullOrEmpty(region))
headers["Region"] = Configuration.ApiClient.ParameterToString(region);
if (!string.IsNullOrEmpty(userId))
headers["User-Id"] = Configuration.ApiClient.ParameterToString(userId);
if (!string.IsNullOrEmpty(Configuration.AccessToken))
headers["Authorization"] = "Bearer " + Configuration.AccessToken;
// Set Accept header
string[] acceptHeaders = new string[] { "application/json" };
var accept = Configuration.ApiClient.SelectHeaderAccept(acceptHeaders);
if (accept != null)
headers["Accept"] = accept;
// Set Content-Type
string[] contentTypes = new string[] { "application/json" };
var contentType = Configuration.ApiClient.SelectHeaderContentType(contentTypes);
// Make the HTTP request
RestResponse response = (RestResponse)await Configuration.ApiClient.CallApiAsync(
path,
Method.Get,
query,
postBody,
headers,
formParams,
fileParams,
pathParams,
contentType
);
if (ExceptionFactory != null)
{
Exception exception = ExceptionFactory("GetProjects", response);
if (exception != null) throw exception;
}
return new ApiResponse<dynamic>(
(int)response.StatusCode,
response.Headers
.GroupBy(x => x.Name)
.ToDictionary(g => g.Key, g => g.First().Value?.ToString()),
Configuration.ApiClient.Deserialize(response, typeof(object))
);
}
}
public class ProjectQueryParams
{
public List<string> Fields { get; set; }
public List<string> Classification { get; set; }
public List<string> Platform { get; set; }
public List<string> Products { get; set; }
public string Name { get; set; }
public List<string> Type { get; set; }
public List<string> Status { get; set; }
public string BusinessUnitId { get; set; }
public string JobNumber { get; set; }
public string UpdatedAt { get; set; }
public string FilterTextMatch { get; set; }
public List<string> Sort { get; set; }
public int? Limit { get; set; }
public int? Offset { get; set; }
public Dictionary<string, string> ToDictionary()
{
var dict = new Dictionary<string, string>();
if (Fields?.Any() == true)
dict["fields"] = string.Join(",", Fields);
if (Classification?.Any() == true)
dict["filter[classification]"] = string.Join(",", Classification);
if (Platform?.Any() == true)
dict["filter[platform]"] = string.Join(",", Platform);
if (Products?.Any() == true)
dict["filter[products]"] = string.Join(",", Products);
if (!string.IsNullOrWhiteSpace(Name))
dict["filter[name]"] = Name;
if (Type?.Any() == true)
dict["filter[type]"] = string.Join(",", Type);
if (Status?.Any() == true)
dict["filter[status]"] = string.Join(",", Status);
if (!string.IsNullOrWhiteSpace(BusinessUnitId))
dict["filter[businessUnitId]"] = BusinessUnitId;
if (!string.IsNullOrWhiteSpace(JobNumber))
dict["filter[jobNumber]"] = JobNumber;
if (!string.IsNullOrWhiteSpace(UpdatedAt))
dict["filter[updatedAt]"] = UpdatedAt;
if (!string.IsNullOrWhiteSpace(FilterTextMatch))
dict["filterTextMatch"] = FilterTextMatch;
if (Sort?.Any() == true)
dict["sort"] = string.Join(",", Sort);
if (Limit.HasValue)
dict["limit"] = Limit.Value.ToString();
if (Offset.HasValue)
dict["offset"] = Offset.Value.ToString();
return dict;
}
}
internal class Program
{
private static string APS_CLIENT_ID = "your_client_id";
private static string APS_CLIENT_SECRET = "your_client_secret";
private static string ACCOUNT_ID = "your_account_id";
private static string USER_ID = "your_user_id";
private static readonly Scope[] SCOPES = new[]
{
Scope.DataRead, Scope.DataWrite, Scope.DataCreate, Scope.DataSearch,
Scope.BucketCreate, Scope.BucketRead, Scope.BucketUpdate, Scope.BucketDelete,
Scope.AccountRead
};
static async Task Main(string[] args)
{
var use3Legged = false; // Flip this to true to use 3-legged OAuth
string accessToken;
if (use3Legged)
{
var authHandler = OAuthHandler.Create(new ForgeConfiguration
{
ClientId = APS_CLIENT_ID,
ClientSecret = APS_CLIENT_SECRET
});
var tcs = new TaskCompletionSource<dynamic>();
authHandler.Start3LeggedOAuthFlow(tkn =>
{
tcs.SetResult(tkn);
});
var token = await tcs.Task;
if (token == null || string.IsNullOrEmpty(token.access_token))
{
Console.WriteLine("Failed to obtain access token. Please check your credentials and try again.");
return;
}
accessToken = token.access_token;
}
else
{
var authHandler = OAuthHandler.Create(new ForgeConfiguration
{
ClientId = APS_CLIENT_ID,
ClientSecret = APS_CLIENT_SECRET
});
var token = await OAuthHandler.Get2LeggedTokenAsync(SCOPES);
accessToken = token.access_token;
}
var accountAdminApi = new AccountAdminApi(new Configuration
{
AccessToken = accessToken,
ApiClient = new ApiClient("https://developer.api.autodesk.com")
});
var result = await accountAdminApi.GetProjectsAsyncWithHttpInfo(
accountId: ACCOUNT_ID,
region: "US",
userId: USER_ID,
queryParams: new ProjectQueryParams { Status = new List<string> { "archived" } }.ToDictionary());
Console.WriteLine("Projects response:");
var formatted = JsonSerializer.Serialize(result.Data, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(formatted);
//Example usage
var queryParams = new ProjectQueryParams
{
Fields = new List<string> { "name", "createdAt", "status" },
Platform = new List<string> { "acc" },
Products = new List<string> { "build", "docs" },
Status = new List<string> { "active", "archived" },
FilterTextMatch = "contains",
Limit = 50,
Offset = 0
};
result = await accountAdminApi.GetProjectsAsyncWithHttpInfo(
accountId: ACCOUNT_ID,
region: "US",
userId: USER_ID,
queryParams: queryParams.ToDictionary());
Console.WriteLine("Projects response:");
formatted = JsonSerializer.Serialize(result.Data, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(formatted);
}
}
internal class ForgeConfiguration
{
public string ClientId { get; set; }
public string ClientSecret { get; set; }
}
internal class OAuthHandler
{
private static ForgeConfiguration _config;
private static readonly Scope[] DefaultScopes = new[] { Scope.DataRead, Scope.DataWrite };
private static readonly ThreeLeggedApiV2 _threeLeggedApi = new ThreeLeggedApiV2();
private static HttpListener _httpListener;
public static OAuthHandler Create(ForgeConfiguration config)
{
_config = config;
return new OAuthHandler();
}
public void Start3LeggedOAuthFlow(Action<dynamic> callback)
{
if (!HttpListener.IsSupported) return;
_httpListener = new HttpListener();
string callbackUrl = $"http://localhost:3000/api/forge/callback/oauth";
_httpListener.Prefixes.Add(callbackUrl.Replace("localhost", "+") + "/");
try
{
_httpListener.Start();
_httpListener.BeginGetContext(OnOAuthCallbackReceived, callback);
var authUrl = _threeLeggedApi.Authorize(_config.ClientId, oAuthConstants.CODE, callbackUrl, DefaultScopes);
var chromePath = FindChromeExecutable();
if (chromePath != null)
{
var psi = new ProcessStartInfo(chromePath)
{
WindowStyle = ProcessWindowStyle.Minimized,
Arguments = $"/incognito {authUrl}"
};
Process.Start(psi);
}
}
catch (Exception ex)
{
Console.WriteLine("OAuthListener error: " + ex.Message);
}
}
private static async void OnOAuthCallbackReceived(IAsyncResult ar)
{
try
{
var context = _httpListener.EndGetContext(ar);
var code = context.Request.QueryString[oAuthConstants.CODE];
byte[] buffer = Encoding.UTF8.GetBytes("<html><body>You can now close this window!</body></html>");
var response = context.Response;
response.ContentType = "text/html";
response.ContentLength64 = buffer.Length;
response.StatusCode = 200;
response.OutputStream.Write(buffer, 0, buffer.Length);
response.OutputStream.Close();
dynamic token = null;
if (!string.IsNullOrEmpty(code))
{
var result = await _threeLeggedApi.GettokenAsyncWithHttpInfo(
_config.ClientId,
_config.ClientSecret,
oAuthConstants.AUTHORIZATION_CODE,
code,
"http://localhost:3000/api/forge/callback/oauth"
);
token = result.Data;
}
((Action<dynamic>)ar.AsyncState)?.Invoke(token);
}
catch (Exception ex)
{
Console.WriteLine("OAuthCallback error: " + ex.Message);
((Action<dynamic>)ar.AsyncState)?.Invoke(null);
}
finally
{
_httpListener.Stop();
}
}
public static async Task<dynamic> Get2LeggedTokenAsync(Scope[] scopes)
{
var oauth = new TwoLeggedApi();
return await oauth.AuthenticateAsync(_config.ClientId, _config.ClientSecret, oAuthConstants.CLIENT_CREDENTIALS, scopes);
}
private static string FindChromeExecutable()
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return null;
const string suffix = @"Google\Chrome\Application\chrome.exe";
var paths = new List<string>
{
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86),
Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles)
};
var regPath = Microsoft.Win32.Registry.GetValue(
@"HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion",
"ProgramW6432Dir", null) as string;
if (!string.IsNullOrEmpty(regPath)) paths.Add(regPath);
return paths.Select(p => Path.Combine(p, suffix)).FirstOrDefault(File.Exists);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment