We're in the process of developing our first model using SAS, and we know we'll want to score it via a web service, presumably done RESTfully, utilizing json for the calls. Another team will be consuming the webservice using clients developed in C#. They were asking if we could generate a Swagger contract based on our eventual web service.
Thus I was wondering if anyone else has implemented C# clients of RESTful services with lots of (100 - 200'ish) parameters involved.
Reason for asking: for instance, a C# client can target a SOAP/XML web service, and during development in Visual Studio, it's pretty much "right-click, point at the URL", and it generates base classes automatically, which is pretty handy to say the least. We'd like to acheive the same sort of easy implementation with our SAS REST service. Swagger preferred, but any comments appreciated.
Just an FYI on this... I was able to generate a C# client to call a MAS REST API (as well as a SASBIWS json stored process). MAS appears to be quite a bit faster for scoring a simple sample model but that's irrelevant to this specific topic.
For MAS, I first generate a ticket; (by HTTP Post to: https://SERVER/SASLogon/rest/VERSION/tickets with UID and PWD parameters... which returns a pre-authenticated URL in a "Location" header; calling that via another http post returns a ticket that can temporarily be used for subsequent calls).
So, I do that and set a breakpoint in Visual Studio order to grab the ticket... then tried NSwagStudio, looking to auto-generate implementation classes... so, basically, targeting this in NSwagStudio...
https://SERVER/SASMicroAnalyticService/rest/modules/MODEL/steps/execute?ticket=TICKET
(where SERVER = my midtier server, and MODEL = the name of what I deployed to MAS from Decision Builder)...
... I was then able to pull a bunch of .json into Swagger... but, unfortunately, it is not in a format for which Swagger can automatically parse and generate C# client classes.
However, it does pretty print ths json, and based on that it was pretty straight-forward to manually implement a generic "ScoringOutput" class, and to map it using a combination of RESTSharp (to make the calls to MAS) and Newtonsoft Json.Net, for parsing the resulting json.
Here's what I did:
The "SAS_ScoringOutput" class:
class SAS_ScoringOutput { public String moduleId = ""; public String moduleName = ""; public String stepId = ""; public IList<SAS_Mappings.SAS_Links> links = new List<SAS_Mappings.SAS_Links>(); public Int64 version = 0; public IList<SAS_Mappings.SAS_NameValuePair> output = new List<SAS_Mappings.SAS_NameValuePair>(); }
which uses two other helper classes...
class SAS_Links { public String method; public String rel; public String href; public String uri; public String type; }
...and...
class SAS_NameValuePair { public String name = ""; public String value = ""; public SAS_NameValuePair(String pName, String pValue) { name = pName; value = pValue; } }
Given all that, and references to both RESTSharp and Json.Net, and a valid ticket... the following should work:
private static SAS_ScoringOutput CallModelScoring(String sasTicket) { SAS_ScoringOutput jsonRsp = null;
// 1. build the request StringBuilder jsonRequest = new StringBuilder("");
// build your json request here... system dependent
IRestClient rstClient1 = new RestClient(); rstClient1.BaseUrl = new Uri("https://SERVER/SASMicroAnalyticService/rest/modules/MODEL/steps/execute?ticket=" + sasTicket); rstClient1.CookieContainer = new System.Net.CookieContainer(); IRestRequest rstReq1 = new RestSharp.RestRequest(); rstReq1.Method = Method.POST; rstReq1.AddHeader("Accept", "*/*"); rstReq1.Parameters.Clear(); rstReq1.AddParameter( "application/json", jsonRequest.ToString(), ParameterType.RequestBody); // 2. Make the Call IRestResponse rstRsp1 = rstClient1.Execute(rstReq1); // 3. Process Results Newtonsoft.Json.Linq.JObject jo = Newtonsoft.Json.Linq.JObject.Parse(rstRsp1.Content); jsonRsp = new SAS_ScoringOutput(); jsonRsp.moduleId = jo.SelectToken("moduleId").ToString(); jsonRsp.moduleName = jo.SelectToken("moduleName").ToString(); jsonRsp.stepId = jo.SelectToken("stepId").ToString(); jsonRsp.version = long.Parse(jo.SelectToken("version").ToString()); foreach (Newtonsoft.Json.Linq.JToken jt in jo.SelectToken("output").Children()) { jsonRsp.output.Add(new SAS_NameValuePair(jt.SelectToken("name").ToString(), jt.SelectToken("value").ToString())); } return jsonRsp; }
There are C# predicate tricks that could reduce the lines of code, and probably other code improvements that could be made, but you get the idea. Also, my names/values are always String... for a model that returns int/double/boolean/date type parameters, you'll have to do the appropriate conversions when mapping your SAS_ScoringOutput class back into the consuming application code layers.
Anyway, hope that helps, if anyone's working on something similar.
Hey John - One way to do this would be by registering your SAS code as a stored process and accessing it through the REST API offered in the BI Web Services interface. I did a little work with this in the past, but only with python, Java, and R. But translating to C# should hopefully not be too difficult. Just take a look at the attached doc and see if it helps you out at all.
I'm assuming this is SAS 9.4. If you have SAS Viya then this would be even easier as it provides a REST API directly (along with APIs for other languages).
Hope this helps.
Brett
Register today and join us virtually on June 16!
sasglobalforum.com | #SASGF
View now: on-demand content for SAS users
I think the question is more about the REST API right?
But beyond that, yes - you should consider feature selection to reduce the number of parameters you need to pass to your web service and model with.
Register today and join us virtually on June 16!
sasglobalforum.com | #SASGF
View now: on-demand content for SAS users
I'm only in charge of operationalization, so the entire model, including feature engineering/reduction is out of my hands. I'm content with my ability to create the REST API, and I can generate a C# client to serve as a reference implementation, (thanks for the document, @BrettWujek - that helps). But am asking about Swagger primilarily based on the same question being asked of me by my eventual C# consumers: they want something that will allow them to auto-generate their consumption classes and then worry only about mapping their core layer classes to the consumption layer classes.
Some browsing shows that others may have utilized Swagger to hit the API and generate what they need automatically; (based on bug reports at Swaggers' github site). I'll give this a try and report back on how it goes.
Thanks for the feedback!
Just an FYI on this... I was able to generate a C# client to call a MAS REST API (as well as a SASBIWS json stored process). MAS appears to be quite a bit faster for scoring a simple sample model but that's irrelevant to this specific topic.
For MAS, I first generate a ticket; (by HTTP Post to: https://SERVER/SASLogon/rest/VERSION/tickets with UID and PWD parameters... which returns a pre-authenticated URL in a "Location" header; calling that via another http post returns a ticket that can temporarily be used for subsequent calls).
So, I do that and set a breakpoint in Visual Studio order to grab the ticket... then tried NSwagStudio, looking to auto-generate implementation classes... so, basically, targeting this in NSwagStudio...
https://SERVER/SASMicroAnalyticService/rest/modules/MODEL/steps/execute?ticket=TICKET
(where SERVER = my midtier server, and MODEL = the name of what I deployed to MAS from Decision Builder)...
... I was then able to pull a bunch of .json into Swagger... but, unfortunately, it is not in a format for which Swagger can automatically parse and generate C# client classes.
However, it does pretty print ths json, and based on that it was pretty straight-forward to manually implement a generic "ScoringOutput" class, and to map it using a combination of RESTSharp (to make the calls to MAS) and Newtonsoft Json.Net, for parsing the resulting json.
Here's what I did:
The "SAS_ScoringOutput" class:
class SAS_ScoringOutput { public String moduleId = ""; public String moduleName = ""; public String stepId = ""; public IList<SAS_Mappings.SAS_Links> links = new List<SAS_Mappings.SAS_Links>(); public Int64 version = 0; public IList<SAS_Mappings.SAS_NameValuePair> output = new List<SAS_Mappings.SAS_NameValuePair>(); }
which uses two other helper classes...
class SAS_Links { public String method; public String rel; public String href; public String uri; public String type; }
...and...
class SAS_NameValuePair { public String name = ""; public String value = ""; public SAS_NameValuePair(String pName, String pValue) { name = pName; value = pValue; } }
Given all that, and references to both RESTSharp and Json.Net, and a valid ticket... the following should work:
private static SAS_ScoringOutput CallModelScoring(String sasTicket) { SAS_ScoringOutput jsonRsp = null;
// 1. build the request StringBuilder jsonRequest = new StringBuilder("");
// build your json request here... system dependent
IRestClient rstClient1 = new RestClient(); rstClient1.BaseUrl = new Uri("https://SERVER/SASMicroAnalyticService/rest/modules/MODEL/steps/execute?ticket=" + sasTicket); rstClient1.CookieContainer = new System.Net.CookieContainer(); IRestRequest rstReq1 = new RestSharp.RestRequest(); rstReq1.Method = Method.POST; rstReq1.AddHeader("Accept", "*/*"); rstReq1.Parameters.Clear(); rstReq1.AddParameter( "application/json", jsonRequest.ToString(), ParameterType.RequestBody); // 2. Make the Call IRestResponse rstRsp1 = rstClient1.Execute(rstReq1); // 3. Process Results Newtonsoft.Json.Linq.JObject jo = Newtonsoft.Json.Linq.JObject.Parse(rstRsp1.Content); jsonRsp = new SAS_ScoringOutput(); jsonRsp.moduleId = jo.SelectToken("moduleId").ToString(); jsonRsp.moduleName = jo.SelectToken("moduleName").ToString(); jsonRsp.stepId = jo.SelectToken("stepId").ToString(); jsonRsp.version = long.Parse(jo.SelectToken("version").ToString()); foreach (Newtonsoft.Json.Linq.JToken jt in jo.SelectToken("output").Children()) { jsonRsp.output.Add(new SAS_NameValuePair(jt.SelectToken("name").ToString(), jt.SelectToken("value").ToString())); } return jsonRsp; }
There are C# predicate tricks that could reduce the lines of code, and probably other code improvements that could be made, but you get the idea. Also, my names/values are always String... for a model that returns int/double/boolean/date type parameters, you'll have to do the appropriate conversions when mapping your SAS_ScoringOutput class back into the consuming application code layers.
Anyway, hope that helps, if anyone's working on something similar.
SAS Innovate 2025 is scheduled for May 6-9 in Orlando, FL. Sign up to be first to learn about the agenda and registration!
Use this tutorial as a handy guide to weigh the pros and cons of these commonly used machine learning algorithms.
Find more tutorials on the SAS Users YouTube channel.