Repository navigation
Expand file tree
/
Copy pathRevitServerApi.cs
More file actions
264 lines (238 loc) · 11.2 KB
/
Copy pathRevitServerApi.cs
File metadata and controls
264 lines (238 loc) · 11.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Text;
using System.Threading.Tasks;
namespace RevitServerNet
{
/// <summary>
/// Main class for working with Revit Server REST API
/// </summary>
public class RevitServerApi
{
private readonly string _baseUrl;
private readonly string _userName;
// Official Admin REST virtual directory naming:
// - version <= 2012: /RevitServerAdminRESTService/AdminRESTService.svc
// - version > 2012: /RevitServerAdminRESTService{YEAR}/AdminRESTService.svc
private static string GetAdminRestServicePath(string serverVersion)
{
if (string.IsNullOrWhiteSpace(serverVersion))
throw new ArgumentException("Server version cannot be null or empty", nameof(serverVersion));
if (!int.TryParse(serverVersion, out var year))
throw new ArgumentException($"Server version '{serverVersion}' is not a valid year.", nameof(serverVersion));
// Library historically targets RS 2012+.
if (year < 2012)
throw new ArgumentException("Server version must be >= 2012.", nameof(serverVersion));
var dir = year <= 2012
? "RevitServerAdminRESTService"
: $"RevitServerAdminRESTService{year}";
return $"/{dir}/AdminRESTService.svc";
}
/// <summary>
/// Initializes a new instance of RevitServerApi
/// </summary>
/// <param name="host">Revit Server host (e.g. "localhost" or IP address)</param>
/// <param name="userName">User name for API requests</param>
/// <param name="useHttps">Use HTTPS (default: false)</param>
/// <param name="serverVersion">Server version (default: "2019")</param>
public RevitServerApi(string host, string userName, bool useHttps = false, string serverVersion = "2019")
{
if (string.IsNullOrEmpty(host))
throw new ArgumentException("Host cannot be null or empty", nameof(host));
if (string.IsNullOrEmpty(userName))
throw new ArgumentException("UserName cannot be null or empty", nameof(userName));
var protocol = useHttps ? "https" : "http";
var servicePath = GetAdminRestServicePath(serverVersion);
_baseUrl = $"{protocol}://{host}{servicePath}";
_userName = userName;
}
/// <summary>
/// Base URL for API
/// </summary>
public string BaseUrl => _baseUrl;
/// <summary>
/// User name
/// </summary>
public string UserName => _userName;
/// <summary>
/// Performs GET request to API
/// </summary>
/// <param name="command">API command</param>
/// <param name="additionalHeaders">Additional headers</param>
/// <returns>JSON response from server</returns>
public async Task<string> GetAsync(string command, Dictionary<string, string> additionalHeaders = null)
{
return await ExecuteRequestAsync("GET", command, additionalHeaders);
}
/// <summary>
/// Performs POST request to API
/// </summary>
/// <param name="command">API command</param>
/// <param name="data">Data to send</param>
/// <param name="additionalHeaders">Additional headers</param>
/// <returns>JSON response from server</returns>
public async Task<string> PostAsync(string command, string data = null, Dictionary<string, string> additionalHeaders = null)
{
return await ExecuteRequestAsync("POST", command, additionalHeaders, data);
}
/// <summary>
/// Performs PUT request to API
/// </summary>
/// <param name="command">API command</param>
/// <param name="data">Data to send</param>
/// <param name="additionalHeaders">Additional headers</param>
/// <returns>JSON response from server</returns>
public async Task<string> PutAsync(string command, string data = null, Dictionary<string, string> additionalHeaders = null)
{
return await ExecuteRequestAsync("PUT", command, additionalHeaders, data);
}
/// <summary>
/// Performs DELETE request to API
/// </summary>
/// <param name="command">API command</param>
/// <param name="additionalHeaders">Additional headers</param>
/// <returns>JSON response from server</returns>
public async Task<string> DeleteAsync(string command, Dictionary<string, string> additionalHeaders = null)
{
return await ExecuteRequestAsync("DELETE", command, additionalHeaders);
}
/// <summary>
/// Encodes path in API format: replaces separators with |, then percent-encodes
/// every folder/model name (segment between |) with <see cref="Uri.EscapeDataString(string)"/>.
/// The | separator itself is kept as is.
/// </summary>
/// <param name="path">Path to file or folder with raw (unescaped) names, e.g. "|#Archive|Model.rvt"</param>
/// <returns>URL path segment ready to put into a command, e.g. "|%23Archive|Model.rvt"</returns>
public static string EncodePath(string path)
{
if (string.IsNullOrEmpty(path))
return "|"; // Root folder
// Replace different separators with |
path = path.Replace('\\', '|').Replace('/', '|');
// Remove double separators
while (path.Contains("||"))
path = path.Replace("||", "|");
// Add | at the beginning if it's not there
if (!path.StartsWith("|"))
path = "|" + path;
// Names go into the URL as data: without escaping '#' cuts the request (fragment),
// '?' starts a query string and '%XX' is decoded by the server into another character.
var segments = path.Split('|');
for (var i = 0; i < segments.Length; i++)
segments[i] = Uri.EscapeDataString(segments[i]);
return string.Join("|", segments);
}
/// <summary>
/// Performs HTTP request to API
/// </summary>
/// <param name="method">HTTP method</param>
/// <param name="command">API command</param>
/// <param name="additionalHeaders">Additional headers</param>
/// <param name="data">Data to send</param>
/// <returns>JSON response from server</returns>
private async Task<string> ExecuteRequestAsync(string method, string command, Dictionary<string, string> additionalHeaders = null, string data = null)
{
var url = $"{_baseUrl}/{command}";
var request = WebRequest.Create(url) as HttpWebRequest;
// Remove debug logging for release
if (request == null)
throw new InvalidOperationException("Cannot create HTTP request");
request.Method = method;
// Enable transparent decompression to reduce payload sizes (if server supports it)
request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
// Set ContentType only for POST/PUT with data
if (method == "POST" || method == "PUT")
{
if (!string.IsNullOrEmpty(data))
{
request.ContentType = "application/json";
}
else
{
// Some RS endpoints require explicit Content-Length: 0
request.ContentLength = 0;
}
}
// Add required headers for Revit Server API
var machineName = Environment.MachineName;
var operationGuid = Guid.NewGuid().ToString();
if (string.IsNullOrEmpty(_userName))
throw new InvalidOperationException("User name cannot be null or empty");
if (string.IsNullOrEmpty(machineName))
throw new InvalidOperationException("Machine name cannot be null or empty");
request.Headers.Add("User-Name", _userName);
request.Headers.Add("User-Machine-Name", machineName);
request.Headers.Add("Operation-GUID", operationGuid);
// Add additional headers if provided
if (additionalHeaders != null)
{
foreach (var header in additionalHeaders)
{
request.Headers.Add(header.Key, header.Value);
}
}
if (!string.IsNullOrEmpty(data) && (method == "POST" || method == "PUT"))
{
var bytes = Encoding.UTF8.GetBytes(data);
request.ContentLength = bytes.Length;
using (var stream = await request.GetRequestStreamAsync())
{
await stream.WriteAsync(bytes, 0, bytes.Length);
}
}
try
{
using (var response = await request.GetResponseAsync() as HttpWebResponse)
using (var stream = response?.GetResponseStream())
{
if (stream == null)
return null;
using (var reader = new StreamReader(stream))
{
return await reader.ReadToEndAsync();
}
}
}
catch (WebException ex)
{
if (ex.Response is HttpWebResponse errorResponse)
{
using (var stream = errorResponse.GetResponseStream())
using (var reader = new StreamReader(stream))
{
var errorContent = await reader.ReadToEndAsync();
throw new RevitServerApiException($"API request failed with status {errorResponse.StatusCode}: {errorContent}", errorResponse.StatusCode, errorContent, ex);
}
}
throw new RevitServerApiException("API request failed", ex);
}
}
}
/// <summary>
/// Exception for Revit Server API errors
/// </summary>
public class RevitServerApiException : Exception
{
public RevitServerApiException(string message) : base(message) { }
public RevitServerApiException(string message, Exception innerException) : base(message, innerException) { }
/// <summary>
/// Creates an exception for an HTTP error response returned by the server
/// </summary>
public RevitServerApiException(string message, HttpStatusCode statusCode, string responseContent, Exception innerException)
: base(message, innerException)
{
StatusCode = statusCode;
ResponseContent = responseContent;
}
/// <summary>
/// HTTP status code of the error response; null when the server returned no HTTP response
/// </summary>
public HttpStatusCode? StatusCode { get; }
/// <summary>
/// Body of the error response; null when the server returned no HTTP response
/// </summary>
public string ResponseContent { get; }
}
}