Metadata Demo
Almost every Archer integration activity involves some inspection of configuration metadata. For example, in order to build a search query or construct a content update JSON object, you'll need to know the relevant module ids, level ids, field ids, values list value ids, etc. For more complex activities you'll need to know a lot more about your Archer metadata than just their id values.
The problem
The Archer API provides mechanisms to discover these attributes, although some are a bit clumsy. The API methods you need to call are not always obvious or intuitive, and the results are returned as either JSON or XML strings which are not documented by Archer. Archer's documentation is great at explaining which methods are available and what calling conventions are needed, but leaves out the details about what actually comes back from each method call. This requires a lot of trial and error investigation as part of the development process in order to figure out just how to find the specific information you're interested in.
For example, you can be pretty certain that (almost) every serialized metadata object returned by Archer's API will have its integer Id in a node named "Id". All entities have this property -- modules, levels, fields, layouts, etc. But what about some esoteric property, like the maximum upload file size allowed on an Attachment field? The documentation says nothing about this, so the only way to know is to retrieve an example Attachment field definition (via the Archer REST API /platformapi/core/system/fielddefinition method), then look at the string returned until you find an attribute that looks like what you want, and take note of it for future use. (BTW, the key name is "MaxFileSize" and it returns an integer expressing the file size in megabytes, but there's nothing in the documentation about this.)
Working with API results
For simple projects, it may suffice to deserialize the results returned by the Archer API into IDictionary<string, dynamic> objects, then pull the needed attributes out using the appropriate string keys (once you know what those keys are). For example, in the above case, you might learn the maximum file size for a given Attachment field like so:
int fieldId = 14604; // Id of the "4th Party Documentation" field in "4th Parties"
string httpResponse = GetHttpResponse(myArcherURL + "/platformapi/core/system/fielddefinition", mySessionToken);
IDictionary<string, dynamic> fieldDefinition = JsonConvert.DeserializeObject<IDictionary<string, dynamic>>(httpResponse);
int maxSizeInMegabytes = fieldDefinition["MaxFileSize"];
assuming that you've already got a valid session token in mySessionToken and have implemented the GetHttpResponse() method with the appropriate HTTP method and GET override for this particular call.
Or you could use Estrelica.Core's APIFacade for that. It already knows that this particular REST API method requires an HTTP POST with a GET override (which some of Archer's API methods do, while others do not). It also knows how to establish and maintain a session, so you don't have to worry about passing session tokens around in your calls. It also manages errors (converting Archer's error responses into exceptions) and retries, and deserializes all JSON strings returned by Archer into IDictionary<string, dynamic> results for you. So the above example becomes as simple as
int fieldId = 14604; // Id of the "4th Party Documentation" field in "4th Parties"
var fieldDefinition = apiFacade.GetFieldDefinition(fieldId);
int maxSizeInMegabytes = fieldDefinition["MaxFileSize"];
The Metadata Demo project shows a few examples of this, demonstrating how to retrieve various metadata entities via REST API methods through the APIFacade class, and how to evaluate the properties of these entities using string key identifiers.
Problems with this approach
If you know all the entity keys of all of the entity properties involved in your, the APIFacade may be a perfect fit for your purpose. However, there are risks involved.
The use of string keys, in addition to requiring the labor of discovery, is generally a bad coding practice. One problem is that they can easily be misspelled in code, but the compiler will have no way to validate them, so you won't learn that there's a problem until runtime. For example, given the scenario above, what if one developer on your team misremembers the "MaxFileSize" key as "MaximumFileSize"? Substituting that erroneous spelling won't cause the build to break, but when it executes at runtime, the dictionary object will throw a key violation exception.
Another problem is that because these keys are arbitrary, the compiler has no way of knowing what data types their corresponding values will have. The examples above use IDictionary<string, dynamic>, where dynamic is just a hint telling the .NET compiler "This is just an object reference, but let me assign it without casting to any specific data type I want". Since by convention we know "MaxFileSize" is an integer, the direct assignment
int maxSizeInMegabytes = fieldDefinition["MaxFileSize"];
works just fine. The compiler trusts us that whatever is in the dictionary for that key is in fact an integer value, allowing the build to complete without error.
Unfortunately, the compiler will also allow us to type something like
DateTime maxSizeInMegabytes = fieldDefinition["MaxFileSize"];
without complaint. At runtime, however, this will cause an exception since an integer cannot be assigned to a DateTime variable, and there's no way for the compiler to know that in advance.
A better way
These are common problems in any API scenario involving remote HTTP calls (where everything must be marshalled across the wire as a string). A common solution to all of this is to deserialize the results not into arbitrary key/value pair dictionaries, but instead into strongly-typed classes, exposing attributes via strongly-typed properties whose data types are known at compile time.
This means that instead of evaluating a value like this:
int maxSizeInMegabytes = fieldDefinition["MaxFileSize"];
you would instead do it like this:
int maxSizeInMegabytes = fieldDefinition.MaxFileSize;
In this case, the compiler knows that the class referenced by the fieldDefinition variable has a property named MaxFileSize, and that it returns an integer value. The compiler will not allow the property name to be misspelled, or permit the result to be assigned to an incompatible type. This drastically reduces coding effort (as the properties themselves will be suggested by IntelliSense, requiring no discovery on the part of the developer) and eliminates the opportunity for runtime errors like those described above.
At least two libraries are available on NuGet which provide a set of deserialization classes for Archer API results. Estrelica.Core provides its own set of these classes, but in a much more holistic and intelligent way.
The Metadata Demo shows parallel implementations for each of the direct APIFacade examples, showing how to do the same thing using Estrelica.Core's metadata object model, requiring far less code in each case and with more trustworthy results due to compile-time type checking.
Working with the Estrelica.Core metadata object model
Estrelica.Core exposes its object model through a series of resolver classes. In the case of metadata, these are exposed via Core.Metadata, which implements the IMetadataResolver interface.
The resolvers are responsible for wrapping calls to Archer's API via the APIFacade, then deserializing the returned results into classes which expose strongly-typed properties via appropriate interfaces. They also offer a plethora of methods to retrieve Archer entities, providing a variety of ways to achieve the same ends. For example, the scenario above could be accomplished via the Metadata resolver as:
int fieldId = 14604; // Id of the "4th Party Documentation" field in "4th Parties"
var fieldDefinition = core.Metadata.FieldById(fieldId) as IAttachmentField;
int maxSizeInMegabytes = fieldDefinition.MaxFileSize;
But this assumes we already know the integer field Id to start with. How did we discover it? Doing that via the Archer API (or even via the APIFacade) would require a series of steps, first retrieving all applications to find the one named "4th Parties", iterating through each of that applications levels, then iterating through each level's fields to find the one named "4th Party Documentation". Only at that point could we retrieve the resulting field definition's "Id" property to know what it is.
The Metadata resolver makes this whole process much simpler, allowing all of these steps to be performed in a single statement:
int maxSizeInMegabytes = core.Metadata.ApplicationByName("4th Parties")
.Fields.ByName<IAttachmentField>("4th Party Documentation")
.MaxFileSize;
This is possible because Estrelica.Core does not merely represent these results as simple discrete serialization classes, the way other libraries do. Estrelica.Core, via its resolver classes, returns a virtual object graph representing your entire Archer instance.
This object graph is fully-traversible, meaning that once you've fetched a given entity (such as an application in the above example), you can visit its related entities (e.g. the Fields associated with the application) via standard properties. Since everything in Archer is related in one way or another to everything else, this traversal can be quite wide-ranging. For example, you could travel from an arbitrary field to the field's level, to the level's module, to the module's solutions, then to another module in that same solution, then to one of that module's levels, then to one of that level's datafeeds, then to one of the fields mapped by that datafeed, all via dotted property notation without making a single explicit call to the Archer API.
The object graph is "virtual" in the sense that it is not completely loaded into memory all at once. Instead, the resolver classes intercept any property calls for these related entities and execute the relevant API calls on demand.
Other considerations
In addition to the examples showing how to work with Archer metadata via the APIFacade and via the Estrelica.Core object model, the Metadata Demo also highlights these two concepts:
- Extensions - Some metadata is not available via the standard Archer API. Estrelica.Core implements its own Extended API which provides access to some of these entities (e.g. Solutions), but it must be activated in order for these calls to be available. The simplest way to activate these extensions (and in fact, the only way if you're evaluating a Trial license) is by simply providing a connection string to your Archer instance database as part of the appSettings.json configuration.
If extensions are available, the Metadata Demo will show how Solutions can be accessed, as well as what happens in the scenario where extensions are unavailable (so you can code defensively for that case).
- Subform discovery - Archer's API has parallel methods for each of the three module types (application, questionnaire and subform) to load their definitions by integer Id, but only exposes "get all" methods for applications and questionnaires. The "get all applications" and "get all questionnaires" methods are used by the Metadata resolver to allow retrieving applications/questionnaires by name, by alias or by Guid, but since there's no corresponding method for subforms, this becomes difficult. The only way to "get all subforms" is by calling the "get subform by Id" method repeatedly, and you can only do this if you already know what all of the subform Ids are.
The Extended API simplifies this by providing a method which returns the Ids of all subforms in the system, and that method is called by the Metadata resolver, if available, to discover the full set of subform Ids whenever it needs to load all subforms. Unfortunately, without the Extended API, the only way to discover these subform Ids is by first encountering them in the "RelatedSubformId" property of a subform field. For this reason, the Metadata resolver will capture this value any time a subform field passes through any its methods, so that the next time a "get all subforms" need arises, it has some subform Ids to work with in the absence of extensions.
Unfortunately this means that when extensions are unavailable, you can't retrieve a subform by name, alias or GUID unless you've already loaded a related subform field at some point (in order to let the Metadata resolver discover that subform's Id). The last few cases in the Metadata Demo application attempt to highlight how all of this works, showing what occurs in regard to subform discovery under different scenarios.