Created
June 17, 2025 08:43
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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