DEV Community

Cover image for Use Revit Design Automation Update Revit Model In ACC (Part 2)
chuongmep
chuongmep

Posted on • Edited on

Use Revit Design Automation Update Revit Model In ACC (Part 2)

Introduction

In last post in had introduced about Use Revit Design Automation Update Revit Model In ACC. Today we will continue with rest of part to finish.
Part 1 : We have done with build the bundle Revit add-in for update assembly code.
Image description

Part 2 : Some step we will go throught:

  • Obtain an Access Token
  • Upload an AppBundle to Design Automation
  • Publish an Activity
  • Submit a WorkItem
  • Download the results

Image description

Obtain an Access Token

This task produces a two-legged OAuth token with a scope sufficient to authenticate the remaining tasks in this walkthrough.

By the end of this task, you will know how to obtain a two-legged access token when the Client ID and Client Secret is known.

You use the following operations in this task:

using Autodesk.SDKManager;
using Autodesk.Authentication.Model;
using Autodesk.Authentication;
string? client_id = System.Environment.GetEnvironmentVariable("APS_CLIENT_ID");
    string? client_secret = System.Environment.GetEnvironmentVariable("APS_CLIENT_SECRET");
SDKManager sdkManager = SdkManagerBuilder
                .Create() // Creates SDK Manager Builder itself.
                .Build();
var _authClient = new AuthenticationClient(sdkManager);
 TwoLeggedToken twoLeggedToken = await _authClient.GetTwoLeggedTokenAsync(client_id, client_secret,
            new List<Scopes>() { Scopes.DataRead, Scopes.BucketRead });
Enter fullscreen mode Exit fullscreen mode

I had post a full topic about Authentication, you can visit at see at
Use Authentication SDK .NET Autodesk Platform Services (APS)

Create a nick name

APS uses the Client ID that was generated in the previous task to uniquely identify the app you created. The Client ID can be long and cryptic, and hence a source of irritation when you reference data you add to your app.

A Nickname is a way to map an app’s Client ID to an easy-to-use name that you can use in place of the Client ID.

You can assign a nickname to an app only if there is no data associated with that app. That is why we are creating the nickname before we do anything else with the app.

By the end of this task, you will know how to create a nickname to reference an app.

You will use the following API endpoint in this task:
1.Include the package in project configuration

 <PackageReference Include="Autodesk.Forge" Version="1.9.9" />
        <PackageReference Include="Autodesk.Forge.DesignAutomation" Version="6.0.1" />
Enter fullscreen mode Exit fullscreen mode

2.Create a class to define configuration for design automation

 DesignAutomateConfiguration configuration = new DesignAutomateConfiguration();
        Autodesk.Forge.Core.ForgeService service =
            new Autodesk.Forge.Core.ForgeService(
                new HttpClient(
                    new ForgeHandler(Microsoft.Extensions.Options.Options.Create(new ForgeConfiguration()
                    {
                        ClientId = configuration.ClientId,
                        ClientSecret = configuration.ClientSecret
                    }))
                    {
                        InnerHandler = new HttpClientHandler()
                    })
            );
        var _designAutomation = new DesignAutomationClient(service);
Enter fullscreen mode Exit fullscreen mode

3.Create a nick name

 private async Task CreateOrUpdateNickName()
    {
        // create nickname
        Console.WriteLine("Start Create NickName");
        string? result = _designAutomation.GetNicknameAsync(_configuration.NickName)?.Result;
        if (string.IsNullOrEmpty(result))
        {
            await _designAutomation.CreateNicknameAsync(_configuration.NickName, new NicknameRecord()
            {
                Nickname = _configuration.NickName,
            });
            Console.WriteLine("Created NickName: " + _configuration.NickName);
        }
        else
        {
            Console.WriteLine("NickName: " + result + " is exist");
            _configuration.NickName = result;
        }

        Console.WriteLine("Created NickName: " + _configuration.NickName);
    }
Enter fullscreen mode Exit fullscreen mode

Upload an AppBundle

An AppBundle is a package of binaries and supporting files that make a Revit add-in.

By the end of this task you will be able to:

  • Put together an AppBundle.
  • Upload an AppBundle to Design Automation.
  • Create an alias for the AppBundle.
  • Create a new version of the AppBundle.
  • Point the alias to the new version of the AppBundle.
private async Task EnsureAppBundle()
    {
        // get the list and check for the name
        Console.WriteLine("Retrieving app bundles");
        Page<string> appBundles = await _designAutomation.GetAppBundlesAsync().ConfigureAwait(false);
        bool existAppBundle = false;
        foreach (string appName in appBundles.Data)
        {
            if (appName.Contains(_configuration.AppName))
            {
                existAppBundle = true;
                Console.WriteLine("Found existing app bundle, start delete: " + appName);
                break;
            }
        }

        if (!existAppBundle)
        {
            // check if ZIP with bundle is here
            Console.WriteLine("Start Create new app bundle");
            if (!File.Exists(_configuration.PackageZipPath))
                throw new Exception("zip bundle not found at " + _configuration.PackageZipPath);

            AppBundle appBundleSpec = new AppBundle()
            {
                Package = _configuration.AppName,
                Engine = _configuration.EngineName,
                Id = _configuration.AppName,
                Description = _configuration.BundleDescription
            };
            AppBundle newAppVersion = await _designAutomation.CreateAppBundleAsync(appBundleSpec).ConfigureAwait(false);
            if (newAppVersion == null) throw new Exception("Cannot create new app");
            Console.WriteLine("Created new bundle: " + newAppVersion.Id);
            // create alias pointing to v1
            string id = _configuration.Alias.ToString().ToLower();
            Autodesk.Forge.DesignAutomation.Model.Alias aliasSpec = new Autodesk.Forge.DesignAutomation.Model.Alias()
                { Id = id, Version = 1 };
            Autodesk.Forge.DesignAutomation.Model.Alias newAlias = await _designAutomation
                .CreateAppBundleAliasAsync(_configuration.AppName, aliasSpec).ConfigureAwait(false);
            Console.WriteLine("Created new alias version: " + newAlias.Id + " pointing to " + newAlias.Version);
            // upload the zip with .bundle
            UploadAppBundleBits(newAppVersion.UploadParameters, _configuration.PackageZipPath).Wait();
        }
    }
Enter fullscreen mode Exit fullscreen mode

Publish an Activity

An Activity is an action that can be executed in Design Automation. You create and post Activities to run specific AppBundles.
Bellow is sample code task to publish an Activity

  private async Task EnsureActivity()
    {
        Console.WriteLine("Start Retrieving activity");
        Page<string> activities = await _designAutomation.GetActivitiesAsync().ConfigureAwait(false);
        bool existActivity = false;
        foreach (string activity in activities.Data)
        {
            if (activity.Contains(_configuration.ActivityFullName))
            {
                existActivity = true;
                Console.WriteLine("Found existing activity, id : " + activity);
                break;
            }
        }

        if (existActivity) return;
        string commandLine =
            $@"""$(engine.path)\\revitcoreconsole.exe"" /al ""$(appbundles[{_configuration.AppName}].path)"" ""$(args[inputFile].path)"" ""$(args[inputJson].path)""";

        Activity activitySpec = new Activity()
        {
            Id = _configuration.ActivityName,
            Appbundles = new List<string>() { _configuration.AppBundleFullName },
            CommandLine = new List<string>() { commandLine },
            Engine = _configuration.EngineName,
            Parameters = new Dictionary<string, Autodesk.Forge.DesignAutomation.Model.Parameter>()
            {
                {
                    "inputFile", new Autodesk.Forge.DesignAutomation.Model.Parameter()
                    {
                        Description = "input revit model infor to run in acc", LocalName = "input.json",
                        Ondemand = false, Required = true,
                        Verb = Verb.Get, Zip = false
                    }
                },
                {
                    "inputJson",
                    new Parameter()
                    {
                        Description = "input data", LocalName = "params.json", Ondemand = false, Required = false,
                        Verb = Verb.Get, Zip = false
                    }
                },
                // {
                //     "result",
                //     new Autodesk.Forge.DesignAutomation.Model.Parameter()
                //     {
                //         Description = "Resulting JSON File",
                //         LocalName = _configuration.ResultFileName,
                //         Ondemand = false,
                //         Required = true,
                //         Verb = Verb.Put,
                //         Zip = false,
                //     }
                // },
            },
            Settings = new Dictionary<string, ISetting>()
            {
                { "script", new StringSetting() { Value = _configuration.Script } }
            },
            Description = _configuration.ActivityDescription,
        };

        Activity newActivity = await _designAutomation.CreateActivityAsync(activitySpec).ConfigureAwait(false);
        Console.WriteLine("Created new activity: " + newActivity.Id);
        // specify the alias for this Activity
        string id = _configuration.Alias.ToString().ToLower();
        Autodesk.Forge.DesignAutomation.Model.Alias aliasSpec = new Autodesk.Forge.DesignAutomation.Model.Alias()
            { Id = id, Version = 1 };
        Autodesk.Forge.DesignAutomation.Model.Alias newAlias =
            await _designAutomation.CreateActivityAliasAsync(_configuration.ActivityName, aliasSpec)
                .ConfigureAwait(false);
        Console.WriteLine($"Created new alias for activity Version: {newAlias.Version} ID: {newAlias.Id}: ");
    }

Enter fullscreen mode Exit fullscreen mode

Submit a WorkItem

When you post a WorkItem to Design Automation, you are instructing Design Automation to execute an Activity.

The relationship between an Activity and a WorkItem can be thought of as a “function definition” and “function call”, respectively. Named parameters of the Activity have corresponding named arguments of the WorkItem. Like in function calls, optional parameters of the Activity can be skipped and left unspecified while posting a WorkItem.

private async Task<WorkItemStatus> ExecuteWorkItem(string data)
    {
        Console.WriteLine("ActivityId: " + _configuration.ActivityFullName);
        // XrefTreeArgument treeArgument = await BuildUploadURL(forgeToken);
        // Console.WriteLine("TreeArgument: " + treeArgument.Url);
        XrefTreeArgument inputJsonArgument = BuildInputJsonArgument(data);
        WorkItem workItem = new WorkItem()
        {
            ActivityId = _configuration.ActivityFullName,
            Arguments = new Dictionary<string, IArgument>()
            {
                { "inputFile", BuildInputRevitModelJsonArgument() },
                // { "result", treeArgument },
                { "inputJson", inputJsonArgument },
                {
                    "onComplete", new XrefTreeArgument
                    {
                        Verb = Verb.Post,
                        Url = _configuration.CallbackUrl
                    }
                },
                {"adsk3LeggedToken", new StringArgument(_configuration.AccessToken3Legged)}
            },
            LimitProcessingTimeSec = 3600,
        };
        Console.WriteLine("Start Create WorkItem");
        WorkItemStatus workItemStatus = await _designAutomation.CreateWorkItemAsync(workItem).ConfigureAwait(false);
        //StartWebsocket(workItem);
        Console.WriteLine("WorkItemStatus: " + workItemStatus.Status);
        Console.WriteLine("Working Item Id: " + workItemStatus.Id);
        //wait until the work item is finished
        while (!workItemStatus.Status.IsDone())
        {
            await Task.Delay(TimeSpan.FromSeconds(2)).ConfigureAwait(false);
            workItemStatus = await _designAutomation.GetWorkitemStatusAsync(workItemStatus.Id).ConfigureAwait(false);
            // if (workItemStatus.Status != Status.Success && workItemStatus.Status != Status.Inprogress)
            // {
            //     Console.WriteLine("Status: " + workItemStatus.Status);
            //     Console.WriteLine(workItemStatus.ReportUrl);
            // }
        }

        Console.WriteLine("WorkItemStatus: " + workItemStatus.Status);
        return workItemStatus;
    }

    private XrefTreeArgument BuildInputJsonArgument(string jsonString)
    {
        // Json pass to Bundle Add-in Revit for execute
        // parse json string to json object
        var inputJson = JsonConvert.DeserializeObject(jsonString)!;
        XrefTreeArgument inputJsonArgument = new XrefTreeArgument()
        {
            Url = "data:application/json, " + ((JArray)inputJson).ToString(Formatting.None).Replace("\"", "'")
        };
        return inputJsonArgument;
    }

    private XrefTreeArgument BuildInputRevitModelJsonArgument()
    {
        // Json pass to Bundle Add-in Revit for execute
        dynamic inputJson = new JObject();
        inputJson.Region = _configuration.Region;
        inputJson.ProjectGuid = _configuration.ProjectGuid;
        inputJson.Leg2Token = _configuration.Leg2Token;
        inputJson.ObjectKey = _configuration.BucketKey;
        inputJson.ObjectId = _configuration.ObjectKey;
        inputJson.ModelGuid = _configuration.ModelGuid;
        XrefTreeArgument inputJsonArgument = new XrefTreeArgument()
        {
            Url = "data:application/json, " + ((JObject)inputJson).ToString(Formatting.None).Replace("\"", "'")
        };
        return inputJsonArgument;
    }
Enter fullscreen mode Exit fullscreen mode

Write report work item job executed

After you submit a WorkItem, you must wait for it to complete and download the results. The code bellow will download txt format report into local for review by print the console output workitem executed.

void WriteReport(WorkItemStatus workItemStatus)
        {
            Console.WriteLine("Status:");
            string json = JsonConvert.SerializeObject(workItemStatus, Formatting.Indented);
            Console.WriteLine(json);
        }

        void WriteLogDetail(WorkItemStatus workItemStatus)
        {
            Console.WriteLine("Log Detail:");
            string reportUrl = workItemStatus.ReportUrl;
            // download txt file from url
            byte[] bytes = new HttpClient().GetByteArrayAsync(reportUrl).Result;
            PrintText(bytes);
        }

        void PrintText(byte[] bytes)
        {
            string resultString = System.Text.Encoding.UTF8.GetString(bytes);
            Console.WriteLine(resultString);
        }
Enter fullscreen mode Exit fullscreen mode

Result

I'm happy when try to build some application to see from user interface, this is how it look like :

Image description

Reference

Top comments (0)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more