Stephen Wilson · Mar 12, 2020 go to post

Thanks guys! I could not find helper links to these methods in the ADO .NET Managed Provider class Intersystems.Data.CacheTypes.CacheStatus so I have to proxy to these Intersystems methods via my own Helper class 

Class App.Status.Helper Extends %RegisteredObject
{
ClassMethod GetOneStatusText(pStatus As %Status) As %String
{
    Quit $system.Status.GetOneStatusText(pStatus)
}
}


This is because by default, when you have an error status, CacheStatus.Message property contains a string formatted as  Error  #5001: CustomerID not valid
 

There's one odd thing in the documentation (Cache 2018.1.3) under %SYSTEM.Status and that is the method signatures for some of these methods

classmethod GetOneErrorText(statuscode As %Status, index As %Integer, language) as %Boolean
classmethod GetOneStatusText(statuscode As %Status, index As %Integer, language) as %Boolean

Surely, with "Text" in the method name the return type should be a %String? Why does it say %Boolean?!
Stephen Wilson · Feb 4, 2020 go to post

Statistical performance metrics isn't what I'm looking for. Rather than a simple number or metric, I'm looking to actually map global structures - similar to what the journal is doing with sets and kills but for a specific routine or set of routines. Also some code doesn't execute but it is still important to identify references to globals.

Stephen Wilson · Jan 17, 2020 go to post

A few of things you could try:

  1. Restore any changes you made to the registry using a backup
  2. Use a system restore point to restore your system to a point before the Caché installation
  3. Use a newer build of Caché eg. Caché 2018.1.3.414 if you are able to.

Alternatively, you might want to contact Intersystems WRC directly or try an installation on a clean system to see if you get the same error. We have a number of small teams in our organisation that use Caché. Our application support team wanted to simplify upgrades to Caché so they designed a simple batch script and published it through SCCM for Windows 10 clients.  The script was based on the 'unattended installation' commands described in the installation guide and involved removing previous Caché versions before installing the desired version. You also might not need the full-kit for your needs - particularly if you are connecting to a remote Caché instance from the Windows 10 client and you use Atelier or VSCode for development.

Stephen Wilson · Oct 30, 2019 go to post

Approach 1

I would be tempted to have your Dispatch class have a forwarding rule for the API version eg v1 or v2 and this will help ensure a clear hierarchical separation both in the URL and in the class definitions between versions. You might also be interested in the %request.URL property for checking relative paths. An example based on your route map might look like 

<Map Prefix="/v1/customer" Forward="MyApp.APIVersion1.Customer" />
<Map Prefix="/v2/customer" Forward="MyApp.
APIVersion2.Customer" />

And your class MyApp.APIVersion1.Customer might look like

<Routes>
        <Route Url="/getcustomer" Method="Post" Call="GetCustomer" />
</Routes>

Personally, I like the classmethod names to reflect the HTTP Method so if I see a GetCustomer method I know that's a HTTP Get method but this is based on personal preference and convention rather than a rule.

Approach 2

The alternative approach is to have everything in the same class but over time this may cause your classes to be rather bloated and unwieldy

<Map Prefix="/v1/customer" Method="Post"  Call="GetCustomerV1" />
<Map Prefix="/v2/customer" Method="Post"  Call="GetCustomerV2" />

Other Thoughts

I do not know if there's a specific function that can be called prior the classmethod in the route map that can validate or invalidate routes. Perhaps the OnPreHTTP method could be used? I noted that some of your methods had the word "default" in them. You can define default route as "/" in your route map.

Stephen Wilson · Oct 23, 2019 go to post

Strange. Our production servers are Caché 2018 on AIX but still showing only 16,384 KB.
Must have preserved the existing setting on upgrades rather than use the new value.
Fresh local Caché install on Windows install shows 256MB though. 

%SYS>w $zv                                                            
Cache for UNIX (IBM AIX for System Power System-64) 2018.1.2 (Build 309_5) Wed Jun 12 2019 20:08:03 EDT                                                        
%SYS>w $zs
16384         
 
Stephen Wilson · Oct 18, 2019 go to post

Try this based on npm link

File Structure
/projects/my-scss
/projects/my-existingproject

Create a new project
cd /projects
mkdir my-scss
cd my-scss

Initialize the project and answer the prompts
npm init

Dump a SCSS file in there

// _base.scss
$font-stack:    Helvetica, sans-serif;
$primary-color: #333;
body {
  font: 100% $font-stack;
  color: $primary-color;
}

Navigate to your existing project
cd ../my-existingproject
npm link ../my-scss

Verify the my-scss folder exists in the node_modules of your existing project.

Then suppose you want to get all the *.scss files in your my-scss project and put them in the /wwwroot/scss folder of my-existingproject. Gulpfile.js within my-existingproject would look something like

const gulp = require('gulp');
const { src, dest } = require('gulp');
const merge = require('merge-stream'); 

var deps = {
    "my-scss": {
        "**/*.scss": ""
    }
};

function scripts() {

    var streams = [];

    for (var prop in deps) {
        console.log("Prepping Scripts for: " + prop);
        for (var itemProp in deps[prop]) {
            streams.push(src("node_modules/" + prop + "/" + itemProp)
                .pipe(dest("wwwroot/scss/" + prop + "/" + deps[prop][itemProp])));
        }
    }

    return merge(streams);
}

exports.scripts = scripts;
exports.default = scripts;

Then providing you have installed gulp and all required gulp-modules run 'gulp' from the project directory command-line. This will run the default task.

Stephen Wilson · Oct 17, 2019 go to post

This might be a stupid answer but here goes! 

It sounds like you are looking for build tools to help you manage your CSS and JavaScript needs (minification, bundling, pollyfillers, CSS generation from SCSS, prefixing) . Have you looked at Gulp or Grunt before? You can use NPM to get them. These tools have a SCSS/SASS module that you can import before writing your various build tasks. 

You can also find good templates for these tools that you can use as 'boilerplate' code that you then customize for your needs. 

Gulp Template

Grunt Template

Another part of your question seemed to refer to using NPM to create local packages that you can then import into various projects. This article describes three solutions:

  1. npm link 
  2. npm install /absolute/path/to/project
  3. npm pack with npm install yourproject.tgz

You can install packages globally using npm install -g <package>  but this is generally used for CLIs 

Stephen Wilson · Sep 20, 2019 go to post

Adding the scope 'offline_access' to the 'password' grant_type generates a refresh_token in the JSON response. 

endpoint: https://{{SERVER}}:{{SSLPORT}}/oauth2/token

{
"grant_type":"password",
"username":"test1",
"password":"P@ssw0rd",
"client_id":"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"client_secret":"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"response_type":"token",
"state":"myapp",
"scope":"myapp openid profile offline_access"    
}
 

Response JSON

{

"access_token": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",

"token_type": "bearer",

"expires_in": 180,

"refresh_token": "7NJ7tQbFBLFcUftZr9j4n6o99Og03QeM6rx51L05eIU",

"scope": "myapp offline_access openid profile",

"account_enabled": 1,

"account_never_expires": 1,

"account_password_never_expires": 1,

"change_password": 0,

"comment": "Test User",

"full_name": "test1",

"roles": "%DB_CODE,createModify,publish"

}

So... if you detect that access_token is no longer valid, you could try using the refresh_token to generate a new one without prompting the user for input. It seems a good idea to have the refresh_token interval significantly larger value than your access_token value. I will need to do more experimentation to find the ideal intervals and review the impact on license usage.

Stephen Wilson · Sep 16, 2019 go to post

This is a common problem. Please bear in mind your system config is specific to you so what is described below may not be the answer.

Initially I used a 3rd party app called CNTLM and pointed Eclipse to the CNTLM process port, which points to the corporate proxy but I would no longer recommend this option as it doesn't account for passwords expiring regularly. 

I later discovered that Basic Proxy Authentication was disabled by default as part of a JRE 8u111 Update under the heading 'Disable Basic authentication for HTTPS tunneling'.  As the document describes, you can override this behavior either 1) globally on your machine if you have the necessary permissions or 2) locally if you have Eclipse installed on a file system you have write access permission.   

Try changing your Eclipse.ini file to include this under after -vmargs

-Djdk.http.auth.tunneling.disabledSchemes="" 

Leave your Network Connections set to 'Native'. Only HTTP Dynamic should be ticked

I would not recommend updating from 1.0 to 1.3 because there has been so many changes since then and projects will need to be migrated. It would be safer to try downloading a fresh install following the instructions to install the plugin and then test your 'Check for Updates' button. 

Using a fresh workspace is also recommended.

On a different note, you can pass proxy login details into the target url if you encode it properly. I've used this trick for Node Package Manager (NPM) configuration in the .npmrc file

proxy=http://DOMAIN%5Cusername:password@myproxyserver.net:8080/

The other common issue you might encounter is the PKIX Path building failed. This is related to HTTPS connections from your JRE running Eclipse and a missing CA certificate from your cacerts certificate store.

Consider logging the issue with WRC if you are looking for a more bespoke solution.

Stephen Wilson · Aug 23, 2019 go to post

That looks a little complex. In contrast to the above example, the default $System.Encryption.GenCryptRand() size appears to be 8 as $L(user.Salt) resolves to 8. After a bit of experimentation, I found I didn't need to encode anything at all. In this example, I'm using user test1 with password P@ssw0rd on a non-unicode 8-bit Cache installation. Use $SYSTEM.Version.IsUnicode() to check your installation

Do ##class(Security.Users).Exists("test1",.user,.status1)
set storedHash = user.Password 
set computed=$System.Encryption.PBKDF2("P@ssw0rd", 1024, user.Salt,20,160)

Produces the following 20 byte hash using PBKDF2 with 1024 iterations, 64 bits of salt and SHA1 (160)

%SYS>zw storedHash
storedHash="n"_$c(138)_"z iSWWs"_$c(11)_"cbM"_$c(27)_"nY'"_$c(3,152)_"H"

%SYS>zw computed
computed="n"_$c(138)_"z iSWWs"_$c(11)_"cbM"_$c(27)_"nY'"_$c(3,152)_"H"
Stephen Wilson · Aug 22, 2019 go to post

I'm simply creating an interface between Caché and a web app that is using the .NET Core Identity Model. The PasswordSignInAsync() method returns a false sign-in result and I'm trying to determine why the password isn't being accepted. 

Stephen Wilson · Aug 13, 2019 go to post

Thanks @Barton Pravin for clarifying scope and providing the code snippets!  If you want the claim to be part of the Token endpoint response message, you can use Do properties.ResponseProperties.SetAt(roles,"roles") 

Heres's a sample of ValidateUser customization

 // Use the Cache roles for the user to setup a custom property.
Set sc=##class(Security.Roles).RecurseRoleSet(prop("Roles"),.roles)
If $$$ISERR(sc) Quit 0


Set roles=prop("Roles")
Do properties.CustomProperties.SetAt(roles,"roles") 

// SETUP CUSTOM CLAIMS HERE
Set tClaim = ##class(%OAuth2.Server.Claim).%New()
Do properties.ResponseProperties.SetAt(roles,"roles")
Do properties.IntrospectionClaims.SetAt(tClaim,"roles")
Do properties.UserinfoClaims.SetAt(tClaim,"roles")
Do properties.JWTClaims.SetAt(tClaim,"roles") 
Do properties.SetClaimValue("roles",roles)

And a sample of the REST API application

Class API.DemoBearerToken Extends %CSP.REST
{
Parameter APIHOST = "localhost";

Parameter APIPORT = 57773;

Parameter APIPATH = "/api/demobearertoken";

Parameter CHARSET = "utf-8";

Parameter CONTENTTYPE = "application/json";

Parameter OAUTH2CLIENTREDIRECTURI = "https://localhost:57773/api/demobearertoken/example";

Parameter OAUTH2APPNAME = "demobearertoken";

XData UrlMap [ XMLNamespace = "http://www.intersystems.com/urlmap" ]
{
<Routes>
<Route Url="/getToken" Method="Get" Call="GetToken"/>
</Routes>
}


Classmethod AccessCheck(Output pAuthorized As %Boolean = 0) as %Status
{

Set dayNum = $p($H,",",1)
Set timeNum = $p($H,",",2)


Set accessToken = ..GetAccessTokenFromRequest(%request) 
Set scope = "createModify openid profile publish" 
Set isValidToken=##class(%SYS.OAuth2.Validation).ValidateJWT(..#OAUTH2APPNAME,.accessToken,,,.jsonValidationObject,.securityParameters,.error)
Set ^LOG(dayNum,timeNum,$UserName,"API.DemoBearerToken",$ztimestamp,"AccessCheck")=$zdatetime(dayNum_","_timeNum,4,1,,,4)_"*"_isValidToken

Set:isValidToken=1 pAuthorized=1
Set:isValidToken=0 pAuthorized=0 
Quit $$$OK
}
ClassMethod GetToken() As %Status
{
#dim %response as %CSP.Response
Set %response.Expires = 86400
Set %response.Headers("Cache-Control") = "max-age=86400"

Set dayNum = $p($H,",",1)
Set timeNum = $p($H,",",2) 
Set ^LOG(dayNum,timeNum,$UserName,"API.DemoBearerToken",$ztimestamp,"GetToken")=$zdatetime(dayNum_","_timeNum,4,1,,,4) 
Set accessToken = ..GetAccessTokenFromRequest(%request)
Set scope = "createModify openid profile publish" 
Set valid=##class(%SYS.OAuth2.Validation).ValidateJWT(..#OAUTH2APPNAME,.accessToken,,,.jsonValidationObject,.securityParameters,.error) 

Set introspectionStatus=##class(%SYS.OAuth2.AccessToken).GetIntrospection(..#OAUTH2APPNAME,accessToken,.introspectionJSON) 
Set userInfoStatus = ##class(%SYS.OAuth2.AccessToken).GetUserinfo(..#OAUTH2APPNAME,accessToken,,.userInfoJSON) 
Set jsonResponse = {}.%Set("OAUTH2APPNAME",..#OAUTH2APPNAME)
Do jsonResponse.%Set("ValidateJWT",valid) 

Do jsonResponse.%Set("jsonValidationObject",jsonValidationObject)
Do jsonResponse.%Set("IntrospectionJSON",introspectionJSON)
Do jsonResponse.%Set("sc_userinfo",$$$ISOK(userInfoStatus)) 

If $$$ISOK(userInfoStatus) {
 Do jsonResponse.%Set("UserInfoJSON",userInfoJSON)
}

Write jsonResponse.%ToJSON()

Quit $$$OK 
}

}

And the Postman response for GET https://{{SERVER}}:{{SSLPORT}}/api/demobearertoken/getToken

{

"OAUTH2APPNAME": "demobearertoken",

"ValidateJWT": 1,

"jsonValidationObject": {

"jti": "https://localhost:57773/oauth2.mxn0URwYVkmaX9BSKHGIzISi-cI",

"iss": "https://localhost:57773/oauth2",

"sub": "test1",

"exp": 1565708268,

"aud": "xxxxxxxxxxxxxxxxxxxxx",

"roles": "%DB_CODE,%Manager,createModify,publish"

},

"IntrospectionJSON": {

"active": true,

"scope": "createModify openid profile publish",

"client_id": "xxxxxxxxxxxxxxxxxxxxxxxxxxx",

"username": "test1",

"token_type": "bearer",

"exp": 1565708268,

"sub": "test1",

"aud": "xxxxxxxxxxxxxxxxxxxxxxx",

"iss": "https://localhost:57773/oauth2",

"roles": "%DB_CODE,%Manager,createModify,publish"

},

"sc_userinfo": 1,

"UserInfoJSON": {

"sub": "test1",

"roles": "%DB_CODE,%Manager,createModify,publish"

}

You can see 'roles' has been added to JWT, Introspection and UserInfo claim types. In my real-world application it's probably sufficient to add it to the JWT. 

Also @Eduard Lebedyuk  suggested using the AccessCheck method to verify the token in the post From Cache how to Retrieve and Use/Reuse a Bearer Token to authenticate and send data to REST web service?  AccessCheck is called before anything else so if the token is invalid or has expired a 401 Unauthorized HTTP Response is returned. A web application consuming this REST API can then process this unauthorized status and return them to the login screen. 

Stephen Wilson · Aug 13, 2019 go to post

I can also see in the classmethod ##class(OAuth2.Server.Token).ReturnToken(client,token) where there is a section on adding customized response properties but where are these set?

My AccessToken.ResponseProperties array appears to be empty

Set json.scope=token.Scope
// Add the customized response properties
Set key=""
For {
Set value=token.Properties.ResponseProperties.GetNext(.key)
If key="" Quit
Set $property(json,key)=value
}
Stephen Wilson · Aug 7, 2019 go to post

You could use https://www.slideshare.net/ or add the document to the GitHub repo.

There is a way to post documents on Intersystems Community under Edit Post -> Change Additional Settings, which I documented here but it's not user friendly and I didn't automatically see links to attached documents within the post so I had to manually add the links. Community feedback suggests they may turn this feature off at some point so I'd recommend any of the above options instead.

Stephen Wilson · Aug 1, 2019 go to post

I edited this post to upload a PDF of the Global Summit 2019 Agenda but I couldn't find a link to the file I uploaded any where in this post - just a blue bar saying "Attached documents". I obtained the following link by going into the edit post view and mousing over the attached file

Global Summit 2019 Grid.pdf

Stephen Wilson · Jun 28, 2019 go to post

Thanks for sharing this.  I would be interested in seeing some of the data structures you decided on with dummy data.

I would typically use a percent (%) global if I wanted a global to be accessible across multiple namespaces but I don't usually mix percent globals and class storage definitions.

Stephen Wilson · Jun 18, 2019 go to post

You could also try a tool like Postman to test service calls and authentication methods.  If you tick the Password checkbox it enables Basic authentication (plain text username/password) is enabled. You can  also use bearer tokens instead, which is a popular authentication scheme.

While authentication/authorization isn't really covered in great detail, REST and Relaxation is a good starting point for REST development and it comes with a video and source code.

You should also double-check your URL is correct and resource permissions are correct. You probably only need permissions on the ENSEMBLE namespace and there might be a resource that defines this. Your URL is probably something like http://yourserver/rest/coffeemakerapp/coffeemaker

Stephen Wilson · May 20, 2019 go to post

I was hoping to convert the above sample into an Internal REST API so that Bearer Authentication could replace Basic Authentication

Stephen Wilson · May 17, 2019 go to post

Great article!

Can you enable HTTP Bearer token authentication on Web Applications via the System Management Portal?

All I see under 'Allowed Authentication Methods' are options for:

  • Unauthenticated
  • Password 
  • Login Cookie

Perhaps I am missing a trick somewhere?

Stephen Wilson · Mar 15, 2019 go to post

What does your Edit Web Application settings look like? What allowed authentication methods are enabled?

Stephen Wilson · Nov 20, 2018 go to post

Pretty embarassing result for me. I misinterpretted how ObjectScript treats equals(=) operator when used with a write and thus lost a lot of marks.  Its never something I've come across but now I am well and truely informed. Good article. From a support perspective, I often have to evaluate expressions in the terminal to check my understanding is correct. It can be mighty fustrating if you spend all day assuming an expression is interpretted one way, only to find your assumption to be incorrect! Agree with Avoid ambiguous or easily misinterrepted code. Also use brackets, they are your friends.

Stephen Wilson · Oct 25, 2018 go to post

Another great resource is the REST and Relaxation webinar.  If your external service returns the above JSON body, you can work with the JSON directly. 

Get the JSON from the HTTP Request Body

    ClassMethod GetJSONData() As %DynamicObject
    {
        // Throw our own exception if the request body does not contain valid JSON.
        Try {
            Set obj = ##class(%DynamicObject).%FromJSON(%request.Content)
        } Catch ex {
            Throw ##class(MyProject.Exception.BadRequest).%New("Invalid JSON")
        }
        Quit obj
    }

Call the method from within your %CSP.REST class. The URL and HTTP method will be your event trigger.

Class MyProject.Patient Extends %CSP.REST
 <Routes>
            <Route Url="/postPatient" Method="Post" Call="PostPatient" />
 </Routes>
ClassMethod PostPatient() As Status
{
   Set formData = ##class(MyProject.API.Service).GetJSONData()   
  
   // Get the tag element
  Set tag = formData.tag
  // Loop through a JSON Array
  Set siteIter = formData.patient.site.%GetIterator()
 
  if ( $GET(siteIter)'=""
  {
            while(siteIter.%GetNext(.key, .value)
            {
                  Set code = value.code
                  Set mrn = value.mrn
            }
  }
return $$$OK
}


The syntax here may not be 100% correct but I hope this gives you some ideas. Try to keep your JSON object as simple as possible
When you get the information from the JSON body you can decide whether to create a new object or modify an existing object using the %Persistent methods %OpenId() %ExistsId() and %New(). I would model your classes based on how you wish to store the data contained within the HTTP request rather than the request itself.  If you are able to obtain information from the request itself eg. "tag: post" then you probably do not need to store that in the database.

You can set the response status you wish to return by

Set %response.Status = ..#HTTP400BADREQUEST   

 so you may not need to encode things in JSON that are part of the request.
 

I use AutoMapper and NewtownsoftJson .NET libraries for mapping JSON to C# objects/models and work with the JSON and ObjectScript classes directly when saving JSON data to the database.

Update: There's some useful documentation in the Using JSON in Caché Guide, including JSON Serialization and de-serialization to dynamic entities using %FromJSON and %ToJSON methods

Stephen Wilson · Oct 24, 2018 go to post

. They are two different platforms and is most likely why you could not find ConfigureServices in the Intersystems docs.  For a Micosoft SQL Server, your ConfigureServices method could have

            services.AddDbContext<MyMSDBContext>(cfg =>
            {

                cfg.UseSqlServer(_config.GetConnectionString("MyConnectionString"));
            });

but I haven't seen any examples in Caché! Perhaps there is a development opportuntiy to write a NuGet package? Here's a Microsoft link to other popular database providers supported within .NET Core.

PS. If you use RESTful HTTP methods to talk to the database then you don't need a database provider as the data can be returned as part of the HTTP Response.

Stephen Wilson · Oct 18, 2018 go to post

My understanding is that the Globals API namespace reference using Intersystems.Globals; is bundled  with the InterSystems.CacheExtreme.dll which relies on the traditional .NET Framework ie. not .NET Core.  Some Global API documentation recommends you use both DLLs mentioned in the question description above.

I am currently developing a .NetCore 2.1 MVC Web Application that uses Bootstrap 4, Datatables.net,  Newtownsoft Json Serailizer (AutoMapper is also good), Moment.js, Tempus Dominus Bootstrap 4 Themed Datepicker,  Caché ObjectScript %Persistent classes mapped to Globals (see The Art of Mapping Globals Series), Cache ObjectScript %CSP.REST classes for mapping web api calls (see Rest and Relatation Demo) and basic authentication over HTTPS. Using this development stack, I do not need to generate proxy classes using the Caché Object Binding Wizard and I have no need for either DLL but I do not have direct access to globals from my .NET classes. The backend classmethods work at the class/object level in response to HTTP requests (Web API calls) to manipulate Global values in the database.

Stephen Wilson · Oct 5, 2018 go to post

If you have never used Wireshark before or haven't a deep understanding of the TCP/IP suite of protocols then Wireshark might be overkill for your needs.  I have only used it in a Lab or Development environment. You also have to consider the disk storage requirements and governance issues around full-packet capture eg. HPIAA, PCI-DSS. You might want to check  Eduard Lebedyuk's article on Debugging Web for additional tools and tips.

For troubleshooting these issues, we enable a monitoring global that records every character recieved within the TCP Stream

START 
  do USE read *CHAR:2 else  do WAIT
  do CHARMONITOR
 goto START
CHARMONITOR
 if ^AC=1 CHAR'=-1 set X=^AC1,^AC1(X+1)="*"_$char(CHAR)_"*"_CHAR_"*",^AC1=X+1 ; Monitor character by character
 set ^CALLED("CHARMON")=$ZD(+$H,4)_"*"_$ZT($P($H,",",2))_"*"_CHAR
 quit

You can try a re-connect using $ZTRAP or try-catch. In this example, we do a maximum of 10 re-connect attempts

 set $ZT="ERROR"
ERROR
 if ($ZE["<READ>")
 {
     set ERRORCOUNT=ERRORCOUNT+1
     if (ERRORCOUNT<11)
     {
         set ^TCPLOG("ERROR",$ZD(+$H,4),$ZT($P($H,",",2)),ERRORCOUNT)=$ZE
         close "|TCP|"_PORT
         hang 30 goto OPEN 
      }
      else do ^%ET }
 }
 else  do ^%ET }
 quit

You might also look at extending the TCP timeout value to see if that makes a difference to the volume of errors. Check out this Ensemble Example of a SOAP Web Service

Stephen Wilson · Oct 4, 2018 go to post

It's a real-world scenario where you can have something as true, false or undefined. In C# .NET you might use bool? type for this.  If you are populating a checkbox HTML element, you will have to consider  the three possible outcomes. Since I'm mapping classes to pre-existing global structures it's important to understand how the object properties map to these pre-existing global structures.