<?xml version="1.0" encoding="utf-8"?>
<rss xmlns:slash="http://purl.org/rss/1.0/modules/slash/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" xmlns:geo="http://www.w3.org/2003/01/geo/wgs84_pos#" xmlns:creativeCommons="http://backend.userland.com/creativeCommonsRssModule" xmlns:feedburner="http://rssnamespace.org/feedburner/ext/1.0" version="2.0">
     <channel>
         <title>Rick Strahl's FoxPro and Web Connection Weblog </title>
         <link>https://west-wind.com/wconnect/weblog/</link>
         <description></description>
         <language>en-us</language>
         <ttl>1440</ttl>       <item>
			<title>What is CORS and how to set it up in West Wind Web Connection</title>
			<pubDate>Mon, 4 Aug 2025 03:38:39 GMT</pubDate>
			<guid isPermaLink="false">57038_20250803</guid>
			<link>https://west-wind.com/wconnect/weblog/ShowEntry.blog?id=57038</link>
			<dc:creator>Rick Strahl</dc:creator>
			<comments>https://west-wind.com/wconnect/weblog/ShowEntry.blog?id=57038#Feedback</comments>
			<slash:comments>0</slash:comments>
			<description>&lt;p&gt;&lt;img src="https://west-wind.com/wconnect/weblog/imageContent/2026/What-is-CORS-and-how-to-set-it-up-in-West-Wind-Web-Connection/CORSBanner.jpg" alt="CORS Banner"&gt;&lt;/p&gt;
&lt;p&gt;CORS stands for &lt;strong&gt;Cross Origin Resource Sharing&lt;/strong&gt; and it's a security feature that you need to be aware of if you're building any Http based REST services that are called from a Web browser and that are accessed across multiple domains. This applies if you have a front-end Web site that runs on one domain, and a back-end server that lives on another domain (or IP address). CORS typically kicks in when making script based requests from a browser for these cross domain calls.&lt;/p&gt;
&lt;p&gt;The protocol is an odd one, in that it's typically enforced only by Web Browsers, and not in play for most other Http clients like say &lt;code&gt;wwHttp&lt;/code&gt; or &lt;code&gt;XMLHttpRequest&lt;/code&gt; in FoxPro, or &lt;code&gt;HttpClient&lt;/code&gt; in .NET unless you explicitly mimic the &lt;code&gt;Origin&lt;/code&gt; and &lt;code&gt;Access-Allow-Request&lt;/code&gt; headers that the protocol uses.&lt;/p&gt;
&lt;p&gt;##AD##&lt;/p&gt;
&lt;p&gt;When a browser makes a cross-origin &lt;code&gt;fetch()&lt;/code&gt; or &lt;code&gt;XMLHttpRequest&lt;/code&gt; call from script, it sends a request for CORS headers which the server should respond to with it's own set of response headers. The browser then checks the serverâ€™s response for specific CORS headers like &lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt; and &lt;code&gt;Access-Control-Allow-Headers&lt;/code&gt;. If the headers match the requesting origin and other headers, the browser allows the calling JavaScript to access the response. If it doesn't match the browser doesn't expose the response to the calling JavaScript Http client and throws a script error on the request.&lt;/p&gt;
&lt;p&gt;The purpose of CORS is to allow the server to tell the Web Browser whether it is allowed access to the requested Url. Although CORS has to be implemented by the server, CORS is really &lt;strong&gt;a Web Browser security feature&lt;/strong&gt; that only is enforced by Web Browsers, but not other Http clients.&lt;/p&gt;
&lt;p&gt;For example the server may want to ensure that requests only come from one or two domains (origins) that are allowed to access the service when called from a browser. Note that CORS doesn't verify or authorize requests - it's merely a protocol feature that sends requesting headers that are confirmed by the server for a follow-on or in-flight request to process.&lt;/p&gt;
&lt;h3 id="web-browser-only-protocol-but-implemented-by-the-server"&gt;Web Browser Only Protocol but implemented by the Server&lt;/h3&gt;
&lt;p&gt;It's an odd protocol because &lt;strong&gt;it's only used in Web Browsers&lt;/strong&gt;, and mostly irrelevant for other Http clients. The reason is that the actual CORS 'security' feature - rejecting a request on invalid or missing CORS data - is implemented by the Web Browser Http client. So it's up to the client to provide this logic and in general only Web Browsers implement CORS security. Other Http never send CORS request nor do they process them or restrict access based on them.&lt;/p&gt;
&lt;h3 id="cors-doesnt-fire-on-localhost"&gt;CORS doesn't fire on localhost&lt;/h3&gt;
&lt;p&gt;It's also easy to forget about CORS during development, because CORS doesn't kick in when running &lt;strong&gt;same origin&lt;/strong&gt; requests. If your Web page and REST Service run on the same domain/IP Address, there's no CORS. During development that's very common, while deployed applications often run on separate servers. Also if you're using Http testing tools like &lt;a href="Https://websurge.west-wind.com"&gt;West Wind WebSurge&lt;/a&gt; or &lt;a href="Https://www.postman.com/"&gt;Postman&lt;/a&gt;, CORS doesn't automatically kick in unless you explicitly provide the CORS request headers like &lt;code&gt;Origin&lt;/code&gt; and any &lt;code&gt;Access-Allow-Request-xxxx&lt;/code&gt; headers. Hence it's easy to forget to test CORS use cases while testing REST applications. Make a point of remembering and/or ensuring you set up specific tests in your Http client request testing suite (you are using that with your services, right? ??)&lt;/p&gt;
&lt;h2 id="the-cors-protocol"&gt;The CORS Protocol&lt;/h2&gt;
&lt;p&gt;CORS is implemented at the Http protocol level via Http headers that are passed from client to server, and back to the client.&lt;/p&gt;
&lt;p&gt;It works like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The client sends an &lt;code&gt;Origin&lt;/code&gt; header and potentially several &lt;code&gt;Access-Allow-Request-xxxx&lt;/code&gt; headers&lt;/li&gt;
&lt;li&gt;This signals that the server should return a CORS response&lt;/li&gt;
&lt;li&gt;The CORS response needs to include at minimum:
&lt;ul&gt;
&lt;li&gt;The domain that is allowed access (ie. the current domain)&lt;/li&gt;
&lt;li&gt;The Http Verbs that are allowed&lt;/li&gt;
&lt;li&gt;The Http Headers that are allowed&lt;/li&gt;
&lt;li&gt;Whether security (Authorization, Cookies) is allowed&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Depending what type of request you're making CORS data is either made via a separate, pre-flight Http &lt;code&gt;OPTIONS&lt;/code&gt; request, or via in-flight headers that are part of a 'normal' request flow.&lt;/p&gt;
&lt;h3 id="pre-flight-options-request"&gt;Pre-flight OPTIONS Request&lt;/h3&gt;
&lt;p&gt;For  most &lt;code&gt;POST&lt;/code&gt;, &lt;code&gt;PUT&lt;/code&gt;, &lt;code&gt;DELETE&lt;/code&gt;, &lt;code&gt;PATCH&lt;/code&gt; operations browsers send a pre-flight &lt;code&gt;OPTIONS&lt;/code&gt; request to the server which requests a CORS header response. In effect this results in &lt;strong&gt;two requests&lt;/strong&gt; being made to the server by the browser: An &lt;code&gt;OPTIONS&lt;/code&gt; request for CORS verification, followed by the original full Http request.&lt;/p&gt;
&lt;p&gt;The Http &lt;code&gt;OPTIONS&lt;/code&gt; request from client requests &lt;strong&gt;only the Http headers&lt;/strong&gt; for the request and the response returned should contain only the Http headers that a server is expected to return for &lt;strong&gt;this request&lt;/strong&gt;, which &lt;strong&gt;includes the CORS headers&lt;/strong&gt;. The response code should return headers, no data and have a result Status Code  &lt;code&gt;204 No Data&lt;/code&gt; although &lt;code&gt;200 OK&lt;/code&gt; with no data also works - the browser doesn't really care about the result code or content, only the headers.&lt;/p&gt;
&lt;p&gt;Here's what that looks like:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://west-wind.com/wconnect/weblog/imageContent/2026/What-is-CORS-and-how-to-set-it-up-in-West-Wind-Web-Connection/PreflightOptionsRequestion.png" alt="Preflight Options Requestion"&gt;&lt;br&gt;
&lt;small&gt;&lt;strong&gt;Figure 1&lt;/strong&gt; - A pre-flight OPTIONS CORS request&lt;/small&gt;&lt;/p&gt;
&lt;p&gt;Note that this is an &lt;code&gt;OPTIONS&lt;/code&gt; request that was originally triggered by a &lt;code&gt;POST&lt;/code&gt; operation that also requires Authentication in this case. Note that the client sets &lt;code&gt;Access-Control-Request&lt;/code&gt; headers to specify what operations it wants to perform which the server response needs to include by adding the corresponding &lt;code&gt;Access-Control-Allow&lt;/code&gt; headers. Remember these are typically sent by a Web browser making a cross-origin request via script code.&lt;/p&gt;
&lt;p&gt;The server has to respond with headers that include the requested origin, headers and methods. If the CORS headers aren't matched the actual request (ie. the &lt;code&gt;POST&lt;/code&gt; request in this case) is never fired and the &lt;code&gt;OPTIONS&lt;/code&gt; request fails the original POST operation that initiated this sequence at the client (ie. the &lt;code&gt;fetch()&lt;/code&gt; or &lt;code&gt;XMLHttpRequest&lt;/code&gt; call).&lt;/p&gt;
&lt;p&gt;To clarify the full request flow in this example is:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Browser makes a &lt;code&gt;fetch()&lt;/code&gt; request with  &lt;code&gt;POST&lt;/code&gt; and Authentication&lt;/li&gt;
&lt;li&gt;Browser fires &lt;code&gt;OPTIONS&lt;/code&gt; request&lt;/li&gt;
&lt;li&gt;Server responds with valid CORS response&lt;/li&gt;
&lt;li&gt;Browser makes the original &lt;code&gt;POST&lt;/code&gt; request&lt;/li&gt;
&lt;li&gt;Server responds to &lt;code&gt;POST&lt;/code&gt; request&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="in-flight-request"&gt;In-flight Request&lt;/h3&gt;
&lt;p&gt;For simple &lt;code&gt;GET&lt;/code&gt; and &lt;code&gt;POST&lt;/code&gt; requests that don't include authentication, cookie or custom headers, an &lt;code&gt;Origin&lt;/code&gt; header is sent without a separate explicit &lt;code&gt;OPTIONS&lt;/code&gt; request and only the single original request is sent. Instead the CORS headers are checked as part of the incoming request and your full response needs to include the CORS headers directly.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://west-wind.com/wconnect/weblog/imageContent/2026/What-is-CORS-and-how-to-set-it-up-in-West-Wind-Web-Connection/InflightPostRequest.png" alt="Inflight Post Request"&gt;&lt;br&gt;
&lt;small&gt;&lt;strong&gt;Figure 2&lt;/strong&gt; - An in-flight CORS request adds CORS headers to the normal request and response headers. &lt;code&gt;Origin&lt;/code&gt; is the trigger.&lt;/small&gt;&lt;/p&gt;
&lt;p&gt;Since in-flight requests don't use a separate request to determine whether the request can execute, it only includes an &lt;code&gt;Origin&lt;/code&gt; header to validate that the domain is allowed. Everything else is moot, because the request is already in process. POST operations sometimes send the &lt;code&gt;Access-Control-Request-Headers&lt;/code&gt;, but it's not required which means you can't just assume to echo back the Headers that were sent - or not add the CORS headers if you decide to not reject the CORS request.&lt;/p&gt;
&lt;p&gt;##AD##&lt;/p&gt;
&lt;h3 id="cors-failure-and-success-responses"&gt;CORS Failure and Success Responses&lt;/h3&gt;
&lt;p&gt;If the client makes a CORS request and your server code decides it doesn't want to allow the request to process, there are number of ways to do this.&lt;/p&gt;
&lt;p&gt;For failures simply return no content with &lt;code&gt;403 Forbidden&lt;/code&gt;, and don't add any of the CORS headers, which effectively fails the CORS request on the client. Any &lt;code&gt;fetch()&lt;/code&gt; or &lt;code&gt;XMLHttpRequest&lt;/code&gt; call will then fail in JavaScript code.&lt;/p&gt;
&lt;p&gt;For a successful &lt;code&gt;OPTIONS&lt;/code&gt; response use &lt;code&gt;204 No Content&lt;/code&gt;, make sure to return the CORS response headers and then exit &lt;strong&gt;before processing the rest of the request&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;For in-flight non-OPTIONS responses, make sure to add the CORS response headers, and continue processing your request as normal. No need to adjust any special Http Response code.&lt;/p&gt;
&lt;blockquote&gt;
&lt;h5 id="--beware-of-access-control-allow-origin-"&gt;&lt;i class="fas fa-warning" style="font-size: 1.1em"&gt;&lt;/i&gt;  Beware of Access-Control-Allow-Origin: *&lt;/h5&gt;
&lt;p&gt;In previous versions of Web Connection (and other server frameworks for that matter) the default generated CORS response for 'all origins allowed'  was to specify &lt;code&gt;*&lt;/code&gt; for the origin value. Although this is a valid value per spec, in recent years several browsers - namely Safari on Mac and iOS - are refusing to accept any * wildcard values for any of the CORS response headers.&lt;/p&gt;
&lt;p&gt;For this reason as of Web Connection 8.5 the templates have changed from the old &lt;code&gt;Access-Control-Allow-Origin: *&lt;/code&gt; syntax to explicitly retrieving the &lt;code&gt;Origin&lt;/code&gt; header and echoing it back in the outgoing header value. The code at the end of this article shows the new approach that works in this more restricted environment.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="cors-in-web-connection"&gt;CORS in Web Connection&lt;/h2&gt;
&lt;p&gt;Web Connection doesn't have direct support for CORS as part of the low level Web Connection Web Server connectors, so CORS support has to be implemented at the application level. It's an optional feature and typically only needed for REST projects, although in  some rare situations you may also need it if you have individual REST requests as part of an HTML application (rare, but it happens).&lt;/p&gt;
&lt;p&gt;When you create a new REST project via the &lt;strong&gt;New Project Wizard&lt;/strong&gt; Web Connection automatically adds CORS support into the generated Process class in &lt;code&gt;OnProcessInit()&lt;/code&gt;. The code checks for an &lt;code&gt;Origin&lt;/code&gt; header and then displays the appropriate CORS response headers, based on the incoming request. This ensures that valid cross-origin requests are properly acknowledged and allowed by the browser.&lt;/p&gt;
&lt;p&gt;The semi generic code to do this lives in your Process class in &lt;code&gt;OnProcessInit()&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;FUNCTION OnProcessInit
LOCAL lcOrigin, lcRequestHeaders, lcVerb

*** Unrelated
Response.Encoding = &amp;quot;UTF8&amp;quot;
Request.lUtf8Encoding = .T.

*** Add CORS headers to allow cross-site access on REST calls for browser access
lcOrigin = Request.ServerVariables(&amp;quot;Http_ORIGIN&amp;quot;)

IF !EMPTY(lcOrigin) 
	*** Allow all domains IP addresses effectively
	Response.AppendHeader(&amp;quot;Access-Control-Allow-Origin&amp;quot;, lcOrigin)  
	Response.AppendHeader(&amp;quot;Access-Control-Allow-Methods&amp;quot;,&amp;quot;POST, GET, DELETE, PUT, OPTIONS&amp;quot;)	
	
	lcRequestHeaders = Request.GetExtraHeader(&amp;quot;Access-Control-Request-Headers&amp;quot;)
	if EMPTY(lcRequestHeaders)
	  lcRequestHeaders = &amp;quot;Content-Type, Authorization&amp;quot; &amp;amp;&amp;amp; ,Cookie if you use cookie auth!
	endif
	Response.AppendHeader(&amp;quot;Access-Control-Allow-Headers&amp;quot;, lcRequestHeaders)
	Response.AppendHeader(&amp;quot;Access-Control-Allow-Credentials&amp;quot;,&amp;quot;true&amp;quot;)
ENDIF

lcVerb = Request.GetHttpVerb()
IF (lcVerb == &amp;quot;OPTIONS&amp;quot;)
    Response.Status = &amp;quot;204 No Content&amp;quot;
	RETURN .F.  &amp;amp;&amp;amp; Stop Processing
ENDIF

*** ... other OnProcessInit() code
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The code first checks for the &lt;code&gt;Origin&lt;/code&gt; client header, which determines whether the request is cross-site and requires the server to return a CORS response. No &lt;code&gt;Origin&lt;/code&gt; means no CORS data is required on the request. So local domain access, or access from a non-Web Browser request won't trigger CORS requests.&lt;/p&gt;
&lt;p&gt;When a CORS request comes in here are the items required:&lt;/p&gt;
&lt;h3 id="original-origins"&gt;Original Origins&lt;/h3&gt;
&lt;p&gt;Next this code implements the default &lt;code&gt;Origin&lt;/code&gt; behavior which returns the origin requested, which &lt;strong&gt;effectively allows access to all domains&lt;/strong&gt; since we're always returning what the client requests.&lt;/p&gt;
&lt;p&gt;If you want to allow only certain domains, you can create some sort of lookup function or even a hard code a list of domains to allow. If a CORS request fails you can return a &lt;code&gt;403 Forbidden&lt;/code&gt; status - and not return the CORS headers.&lt;/p&gt;
&lt;p&gt;Here's what that looks like:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;IF !EMPTY(lcOrigin) 
    IF !THIS.CheckForValidOrigin(lcOrigin)
       Response.Status = &amp;quot;403 Forbidden&amp;quot;
       RETURN .F.
    ENDIF 
    
	Response.AppendHeader(&amp;quot;Access-Control-Allow-Origin&amp;quot;, lcOrigin)  
    ...
ENDIF
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="methods"&gt;Methods&lt;/h3&gt;
&lt;p&gt;You need to specify the allowed methods when OPTIONS requests are made. In OPTIONS requests the client will send &lt;code&gt;Access-Control-Request-Methods&lt;/code&gt; which you can echo back. Not though that this value is not present in inflight requests so it's better to use a fixed set of Http Verbs that you are planning to use in your project.&lt;/p&gt;
&lt;p&gt;Easiest just to return all the methods your app supports, but you could also return the specific verb being requested.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;Response.AppendHeader(&amp;quot;Access-Control-Allow-Methods&amp;quot;,&amp;quot;POST, GET, DELETE, PUT, OPTIONS&amp;quot;)	
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note that in an &lt;code&gt;OPTIONS&lt;/code&gt; command you need to include &lt;code&gt;OPTIONS&lt;/code&gt; plus &lt;code&gt;Access-Control-Request-Methods&lt;/code&gt; rather than just using &lt;code&gt;Request.GetHttpVerb()&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id="headers"&gt;Headers&lt;/h3&gt;
&lt;p&gt;This one is a little tricky since you may not know what headers you need to support for all requests. &lt;code&gt;OPTIONS&lt;/code&gt; requests provide an explicit &lt;code&gt;Access-Control_Request-Header&lt;/code&gt; value that you can echo back, but again there's no guarantee that this value exists so you want to make sure you have a default value ready using this logic:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;lcRequestHeaders = Request.GetExtraHeader(&amp;quot;Access-Control-Request-Headers&amp;quot;)
IF EMPTY(lcRequestHeaders)
  lcRequestHeaders = &amp;quot;Content-Type, Authorization&amp;quot; &amp;amp;&amp;amp; ,Cookie if you use cookie auth or Sessions!
ENDIF
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The tricky bit here is that &lt;strong&gt;you need to provide all custom headers&lt;/strong&gt; that you might send. While that may seem easy for individual requests, it's more difficult to figure out exactly what's needed for &lt;em&gt;every request&lt;/em&gt; in an application and distill that down to a single set of headers. Hence you'll want to use the requested headers if available. If you have custom headers it'll always trigger an &lt;code&gt;OPTIONS&lt;/code&gt; request, so in that case the &lt;code&gt;Allow-Control-Request-Headers&lt;/code&gt; should be sent with the incoming request and that's what you should return in your CORS header response.&lt;/p&gt;
&lt;h3 id="allow-credentials"&gt;Allow Credentials&lt;/h3&gt;
&lt;p&gt;If your app has any authentication you'll want to set this value to true. This is needed if you have &lt;code&gt;Authorization&lt;/code&gt; or &lt;code&gt;Cookie&lt;/code&gt; headers.&lt;/p&gt;
&lt;p&gt;Kind of silly that this is required since the headers should be able to determine whether this is required.&lt;/p&gt;
&lt;p&gt;##AD##&lt;/p&gt;
&lt;h2 id="summary"&gt;Summary&lt;/h2&gt;
&lt;p&gt;CORS is a clunky protocol and when you first look at it, it doesn't seem to be very effective at providing any security at all. However, for browsers it is useful in ensuring that errand Web browsers can't spoof requests from an embedded iframe for example. The server can explicitly check valid source domains and refuse to serve requests if the list of client domains is not met. However, that does not negate the problem because non-Web Browser clients can call your server any way they want - including potentially spoofed origin domains.&lt;/p&gt;
&lt;p&gt;The good news is that it's easy to implement CORS for your REST services in Web Connection. There's only a little bit of code required, and in can be placed into a single entry point in &lt;code&gt;OnProcessInit()&lt;/code&gt; in one place. If you're creating new REST projects, Web Connection automatically provides the CORS code in the generated process class and if you have existing code you can easily copy in the code from this article...&lt;/p&gt;
&lt;div style="margin-top: 30px;font-size: 0.8em;
            border-top: 1px solid #eee;padding-top: 8px;"&gt;
    &lt;img src="Https://markdownmonster.west-wind.com/favicon.png" style="height: 20px;float: left; margin-right: 10px;"&gt;
    this post created and published with the 
    &lt;a href="Https://markdownmonster.west-wind.com" target="top"&gt;Markdown Monster Editor&lt;/a&gt; 
&lt;/div&gt;
</description>
     </item>
     <item>
			<title>Web Connection 8.4 Release Post</title>
			<pubDate>Wed, 23 Jul 2025 04:32:21 GMT</pubDate>
			<guid isPermaLink="false">57037_20250722</guid>
			<link>https://west-wind.com/wconnect/weblog/ShowEntry.blog?id=57037</link>
			<dc:creator>Rick Strahl</dc:creator>
			<comments>https://west-wind.com/wconnect/weblog/ShowEntry.blog?id=57037#Feedback</comments>
			<slash:comments>0</slash:comments>
			<description>&lt;p&gt;&lt;img src="https://webconnection.west-wind.com/images/WebConnection_Code_Banner.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Hi all,&lt;/p&gt;
&lt;p&gt;I've released Web Connection 8.4 which is a minor maintenance release of the FoxPro Web and Service Development framework. The primary reason for this release are several small bug fixes that might be impacting some of you if you are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Using wwUserSecurity Authentication (especially in Virtual Folders)&lt;/li&gt;
&lt;li&gt;You're using Multipart Form Uploads via wwHttp&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If that's you and you're using v8.1-8.3, you'll probably want to update to the latest version as soon as possible.&lt;/p&gt;
&lt;p&gt;There are also a couple of nice enhancements in wwDotnetBridge, and a new documentation viewer offline application.&lt;/p&gt;
&lt;h2 id="bug-fixes"&gt;Bug Fixes&lt;/h2&gt;
&lt;p&gt;Let's start with the important bug fixes because they are the main reason for this release.&lt;/p&gt;
&lt;h3 id="fix-user-authentication-session-cookie-bug"&gt;Fix User Authentication Session Cookie Bug&lt;/h3&gt;
&lt;p&gt;In the last 8.2-8.3 a regression error was introduced that in certain situations would not properly set the wwUserSecurity related authentication Session cookie. Specifically this can be a problem in scenarios where you're using a non-root virtual folder in your Web application.&lt;/p&gt;
&lt;p&gt;I'm not quite sure why this particular error occurred only with virtual folders since Web Connection cookies are always set on the domain root, but for some reason cookies set when running under the virtual in some cases would not be set properly. The issue basically was that a session Id was present but for some reason the session was not recovered and so a new session key gets generated on each check attempt which effectively results in a successful login that lasts only for the current requests. Oddly this only occurs in virtuals and then more likely on nested virtuals (for me it happened in the &lt;a href="https://west-wind.com/wconnect/weblog"&gt;Web Connection Weblog&lt;/a&gt; specifically).&lt;/p&gt;
&lt;p&gt;It's fixed now where the code now explicitly generates a new cookie on login rather than checking for an existing cookie to update.&lt;/p&gt;
&lt;h3 id="fix-wwhttp-multipart-form-data-content-type-not-getting-set"&gt;Fix: wwHttp Multipart Form Data Content Type not getting set&lt;/h3&gt;
&lt;p&gt;This issue is another regression that cropped up from the recent work to support greater than 16mb uploads and downloads. Essentially, the Content Type parameter on &lt;a href="https://webconnection.west-wind.com/docs/Utility-Classes/West-Wind-Internet-Protocols/Class-wwHTTP/wwHTTPAddPostKey.html"&gt;AddPostKey()&lt;/a&gt; was not being passed through in to the request. It does work for the newly added &lt;code&gt;AddPostFile()&lt;/code&gt; but if you're using older code that uses &lt;code&gt;AddPostKey()&lt;/code&gt; to upload files the content type was not being appended.&lt;/p&gt;
&lt;p&gt;In the process of fixing this bug, I also cleaned up the signature to the &lt;a href="https://webconnection.west-wind.com/docs/Utility-Classes/West-Wind-Internet-Protocols/Class-wwHTTP/wwHTTPAddPostFile.html"&gt;AddPostFile()&lt;/a&gt; method. The method initially had inherited the same signature as &lt;code&gt;AddPostKey()&lt;/code&gt; but there were several parameters that are superfluous for uploading files, so the signature was adjusted for more logical flow.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The change to &lt;code&gt;AddPostKey&lt;/code&gt; is a potential breaking change if you were using the method since the order of parameters has changed.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="missing-jsonservice-variable-in-wwrestprocess-process-methods"&gt;Missing JsonService Variable in wwRestProcess Process Methods&lt;/h3&gt;
&lt;p&gt;Another regression bug relates to &lt;a href="https://webconnection.west-wind.com/docs/Framework-Classes/Class-wwRestProcess.html"&gt;wwRestService&lt;/a&gt; and the use of the intrinsic &lt;code&gt;JsonService&lt;/code&gt; variable that is passed as a shortcut for &lt;code&gt;THIS.oJsonService&lt;/code&gt; into a REST Process method. The error was due a missing &lt;code&gt;PRIVATE&lt;/code&gt; scope assignment at the top of the processing pipeline in &lt;code&gt;RouteRequest()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This has been fixed.&lt;/p&gt;
&lt;h2 id="new-and-improved-features"&gt;New and Improved Features&lt;/h2&gt;
&lt;p&gt;Not much to report in this update but there are few.&lt;/p&gt;
&lt;h3 id="wwdotnetbridge-unblock-all-dlls-loaded"&gt;wwDotnetBridge: Unblock all DLLs loaded&lt;/h3&gt;
&lt;p&gt;The classic .NET Framework still uses Windows Vista Style zoning security by default and so respects the custom blocking attributes that are added to binary files downloaded from the Internet (including files embedded in Zip or 7zip files when unpacked), media devices or from non-local domain drives. Files from those sources get implicitly marked by Windows as 'blocked'.&lt;/p&gt;
&lt;p&gt;wwDotnetBridge has for some time explicitly removed blocks on &lt;code&gt;wwDotnetBridge.dll&lt;/code&gt; which is the initially loaded Dll that loads the runtime. In most cases that's enough, but I've recently run into cases where other DLLs being loaded were also affected and failed when calling &lt;code&gt;loBridge.LoadAssembly()&lt;/code&gt;.  For this reason, wwDotnetBridge now also removes blocks from any assembly loaded via &lt;code&gt;LoadAssembly()&lt;/code&gt; before loading.&lt;/p&gt;
&lt;p&gt;This should improve issues with blocked DLL loading significantly, but you may &lt;strong&gt;still run into problems&lt;/strong&gt; if the DLLs loaded load other DLLs implicitly. In that case you can explicitly load those assemblies using &lt;code&gt;LoadAssembly()&lt;/code&gt; (starting with the most deeply nested dependency first), or you can manually unblock files as described in this &lt;a href="https://webconnection.west-wind.com/docs/Utility-Classes/Class-wwDotnetBridge/Unable-to-load-CLR-Instance-Error.html"&gt;help topic.&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="new-offline-documentation-viewer"&gt;New Offline Documentation Viewer&lt;/h3&gt;
&lt;p&gt;For years several people here - looking at you Tore - have hounded me to provide offline documentation again as we used some time ago. In the past I created a very large PDF document which was problematic to create as I used Word Automation to import from Html and then Print to Pdf. That process was very brittle and took a really long time to run and often would randomly fail.&lt;/p&gt;
&lt;p&gt;Well, recently I switched the documentation over to a new documentation system I've been building called &lt;a href="https://documentationmonster.com"&gt;Documentation Monster&lt;/a&gt;, which is a Help Builder like system built ontop of my popular &lt;a href="https://markdownmonster.west-wind.com"&gt;Markdown Monster&lt;/a&gt; editor.&lt;/p&gt;
&lt;p&gt;As part of that tool there's a new option to create a self contained documentation viewer application that can be used to browse the online documentation offline.&lt;/p&gt;
&lt;p&gt;Here's what that looks like for Web Connection:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://webconnection.west-wind.com/docs/images/WebConnectionDocumentationViewer.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;All the functionality of the Web site is available in the offline Viewer so you get all the same docs - offline.&lt;/p&gt;
&lt;p&gt;The big benefit from my end is that's easy and fast to build so this documentation is much easier to keep in sync with the online documentation.&lt;/p&gt;
&lt;p&gt;The tool produces a self contained EXE that itself is very small. The size of the EXE is determined primarily by the size of the documentation which is embedded inside of the Exe and unpacked when run. That said the Web Connection file is still in the 20mb range zipped due to the huge amount of content and media. The Exe has no dependencies as it runs on the built-in .NET framework.&lt;/p&gt;
&lt;p&gt;You can check it out from here:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://webconnection.west-wind.com/docs/West-Wind-Web-Connection/Offline-Documentation.html"&gt;Download the Web Connection Documentation Offline Viewer&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="summary"&gt;Summary&lt;/h2&gt;
&lt;p&gt;Overall this is a very small release that's primarily been released for the bug fixes (which have been avaiable for a while in the Experimental Updates Zip file as soon as they were discovered and fixed).&lt;/p&gt;
&lt;p&gt;I would recommend updating to the latest version if you are on 8.1-8.3.&lt;/p&gt;
</description>
     </item>
     <item>
			<title>Creating and Debugging .NET Assemblies for wwDotnetBridge and Visual FoxPro</title>
			<pubDate>Fri, 23 May 2025 06:49:41 GMT</pubDate>
			<guid isPermaLink="false">57036_20250522</guid>
			<link>https://west-wind.com/wconnect/weblog/ShowEntry.blog?id=57036</link>
			<dc:creator>Rick Strahl</dc:creator>
			<comments>https://west-wind.com/wconnect/weblog/ShowEntry.blog?id=57036#Feedback</comments>
			<slash:comments>1</slash:comments>
			<description>&lt;p&gt;&lt;img src="https://west-wind.com/wconnect/weblog/imageContent/2025/CreatingAndDebuggingDotnetAssembliesForwwDotnetBridge/HackingBanner.jpg" alt="Hacking Banner"&gt;&lt;/p&gt;
&lt;p&gt;If you're using &lt;a href="https://github.com/RickStrahl/wwDotnetBridge"&gt;wwDotnetBridge&lt;/a&gt; with FoxPro it's quite likely that at some point you'll want to create your own .NET Dlls that interface with external .NET code. While wwDotnetBridge allows you to interface with .NET directly, the code you have to write can be quite tedious because for many things you have explicitly reference the intrinsic methods like &lt;code&gt;InvokeMethod()&lt;/code&gt;, &lt;code&gt;SetProperty()&lt;/code&gt;, &lt;code&gt;GetProperty()&lt;/code&gt; to interact with data that is not compatible over straight COM. While most of these things can be done from FoxPro, the process of doing so is often verbose and in some cases requires multiple objects to be passed around. These intrinsic type scenarios are also relatively slow - compared to native code that is directly executed from .NET. At minimum every .NET call involves a COM call between FoxPro and .NET and when using the Intrinsic methods using Reflection in .NET. It's not horribly slow but compared to native .NET code directly called from .NET it's definitely slower.&lt;/p&gt;
&lt;p&gt;Additionally using .NET natively gives you much easier discovery of what's available via the .NET IDE tools that provide rich Intellisense, code completion, CoPilot, Refactoring and easy code navigation. All of this adds up to a richer experience.&lt;/p&gt;
&lt;p&gt;Now if you're making only one or two calls into a .NET library from FoxPro it's probably not worth creating a separate .NET component. But if you're making a lot of calls back and forth or the code is performance sensitive, then it may very well be worth your time to create a wrapper .NET compoent that you make one or two consolidated calls to and which then handles many .NET operations or entire component functionality.&lt;/p&gt;
&lt;p&gt;I am a big fan of building wrapper components in .NET where you can pass a few key values of input to the wrapper from FoxPro using COM optimized types, and the wrapper then does the brunt of work using native .NET code. If you look at most West Wind components like &lt;code&gt;wwSmtp&lt;/code&gt;, &lt;code&gt;wwFtpClient&lt;/code&gt;, &lt;code&gt;MarkdownParser&lt;/code&gt;, this is exactly how those components are built - there's a .NET class that exposes most of the functionality with a few very focused method calls that combine hundreds of .NET calls and a FoxPro front end abstraction that calls into the wrapper. This is by far my preferred way of exposing functionality from .NET libraries to FoxPro as it optimizes each platform for what it does best.&lt;/p&gt;
&lt;p&gt;I know many of you old FoxPro dogs want to do everything in FoxPro, but one of the big advantages of wwDotnetBridge is that it lets you bridge the gap between FoxPro and .NET. It provides an easy on-ramp for experimenting and integrating .NET code into your own applications one piece of code at a time. Whether you implement a single .NET method that you call from FoxPro, a full class, or a whole complex API of components or business objects - you can choose your own pace. Because you're calling a library you can choose how modular you want to go. It's a practical way to create small islands of functionality outside of FoxPro that still can easily integrate with your FoxPro application transparently.&lt;/p&gt;
&lt;p&gt;##AD##&lt;/p&gt;
&lt;h2 id="creating-your-own-net-components"&gt;Creating your own .NET Components&lt;/h2&gt;
&lt;p&gt;So when you build a .NET component to interface with, you're going to create a &lt;strong&gt;Library Project&lt;/strong&gt; in .NET. A .NET Class library is essentially library of potentially unrelated classes that doesn't have a 'startup' class. A library is a project without an entry point: It's not a Desktop or Web or Console project, but it's just one or more .NET types (classes, structs, enums etc.) that can be called directly from .NET or... from FoxPro using wwDotnetBridge.&lt;/p&gt;
&lt;p&gt;Any .NET class library that you create can be instantiated and called via wwDotnetBridge. IOW - you create a class in .NET, put it somewhere FoxPro can find it and call it with wwDotnetBridge:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;do wwDotnetBridge  &amp;amp;&amp;amp; load lib
loBridge = GetwwDotnetBridge()

llResult = loBridge.LoadAssembly(&amp;quot;bin\MyFirstLibrary.dll&amp;quot;)
? loBridge.cErrorMsg  &amp;amp;&amp;amp; if there's an error and llResult is false

loPerson = loBridge.CreateInstance(&amp;quot;MyFirstLibrary.Person&amp;quot;)
loPerson.Firstname = &amp;quot;Rick&amp;quot;
loPerson.Lastname = &amp;quot;Strahl&amp;quot;
loPerson.Address = loBridge.CreateInstance(&amp;quot;MyFirstLibrary.Address&amp;quot;)
loPerson.Address.Street = &amp;quot;999 Emergency&amp;quot;
loPerson.Address.PostalCode = &amp;quot;97000&amp;quot;
loPerson.Entered = DATETIME()
loPerson.Save()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To create a minimal .NET project literally takes two files: A project file and a source code file that contains one or more classes  that can be called. If you need more classes or components you can add them to the same file or create new classes. The new .NET project system automatically picks up and compiles all source files it finds so it's easy to add functionality.&lt;/p&gt;
&lt;p&gt;There are a number of ways to create a new project, but due to the fact that recent versions of Visual Studio and the &lt;code&gt;dotnet new&lt;/code&gt; CLI don't include templates for new .NET Framework (net4.x) projects.&lt;/p&gt;
&lt;p&gt;While you can use the tooling, but my preferred way of doing this is just creating the project file and the main class by hand (or &lt;a href="https://github.com/RickStrahl/swfox2024-wwdotnetbridge-revisited/tree/master/Dotnet/New%20Project%20net472%20Template"&gt;copying an existing project template from here&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;There are two reasons for this :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;.NET Tools (Visual Studio and the dotnet CLI) don't support creating new .NET 4.x projects&lt;/li&gt;
&lt;li&gt;It's easy to copy a couple of files to disk&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Although neither Visual Studio, the &lt;code&gt;dotnet&lt;/code&gt; command line or VS Code support creating .NET Framework projects, you can definitely build them once created. They are just hiding .NET Framework projects because Microsoft is trying to push everyone to .NET Core.&lt;/p&gt;
&lt;p&gt;However, I &lt;strong&gt;highly recommend&lt;/strong&gt; that you create .NET Framework (.NET 4.x) projects rather than .NET Core projects for your components unless you have a pressing need for .NET Core features or you need to interface with another library that does not support .NET 4.x or .NET Standard 2.0 which both can be used with the built-in .NET Framework.&lt;/p&gt;
&lt;p&gt;The reason is simply that the .NET Framework is built-into Windows so there's nothing to install. You can create a .NET Framework project compile and run without any other requirements other than the tiny DLL created (plus any dependent assemblies your component references). .NET Core on the other hand requires that a compatible runtime is installed, which means you have to make sure that your application checks and deploys the right runtime. There's no such requirement for .NET Framework.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;If you build for .NET Framework chances are that you can also move the code to .NET Core easily by adding a second target, so this is not a 1-way street or you can multi-target and actually support both. Difference between them are slight especially if you stick to core functionality.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;##AD##&lt;/p&gt;
&lt;h3 id="creating-a-new-project-by-hand"&gt;Creating a new Project 'by hand'&lt;/h3&gt;
&lt;p&gt;There are a number of ways of creating a .NET project, but here's what I do:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Create a new folder&lt;/li&gt;
&lt;li&gt;Open Visual Studio Code in that folder initially (or any other editor)&lt;/li&gt;
&lt;li&gt;Create the following two Files in there (Project and first component)&lt;/li&gt;
&lt;li&gt;Use the command line tools to build and run to test the component&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I do this even if I have Visual Studio installed. Why? As mentioned neither Visual Studio or the &lt;code&gt;dotnet new&lt;/code&gt; CLI has support for .NET framework. While you can create a new project for &lt;code&gt;netstandard2.0&lt;/code&gt; which is &lt;em&gt;close&lt;/em&gt; to what you need - you need to make a few small changes to the project. So rather than screw around with this changing template and remove things that don't work,
I use either a template from disk or manually create the files and copy in the small bits of project and initial class code.&lt;/p&gt;
&lt;p&gt;Start by creating creating a new folder for your project and opening an editor there:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-ps"&gt;# create and goto  directory
cd D:\wwapps\Conf\wwDotnetBridgeRevisited\Dotnet\FirstLibrary

# Open VS Code in folder mode
code .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Create two files:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;FirstProject.csproj&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Person.cs&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here's the project:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-xml"&gt;&amp;lt;!-- FirstLibrary.csproj --&amp;gt;
&amp;lt;Project Sdk=&amp;quot;Microsoft.NET.Sdk&amp;quot;&amp;gt;

  &amp;lt;PropertyGroup&amp;gt;    
    &amp;lt;TargetFramework&amp;gt;net472&amp;lt;/TargetFramework&amp;gt;    

    &amp;lt;!-- Optional: Output to a specific folder. Relative or absolute --&amp;gt;    
    &amp;lt;OutputPath&amp;gt;..\..\bin&amp;lt;/OutputPath&amp;gt;
    &amp;lt;AppendTargetFrameworkToOutputPath&amp;gt;false&amp;lt;/AppendTargetFrameworkToOutputPath&amp;gt;
   
  &amp;lt;/PropertyGroup&amp;gt;
&amp;lt;/Project&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This sets the target .NET 4.72 or later and puts the output DLL into the specified folder. The folder is optional - if you don't specify it goes into a deep folder hierarchy below the project. Typically you don't want this.&lt;/p&gt;
&lt;p&gt;Personally I like to put all my .NET assemblies either into my application's root folder (if there's only one) or &lt;code&gt;.\BinSupport&lt;/code&gt; folder below it for many Dlls.&lt;/p&gt;
&lt;p&gt;Next you need to create at least a single &lt;code&gt;.cs&lt;/code&gt; source file with a class in it. I'll create the &lt;code&gt;Person&lt;/code&gt; and &lt;code&gt;Address&lt;/code&gt; classes I used in the previous example here:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;using System;
using System.Collections.Generic;

namespace FirstLibrary
{
    public class Person
    {
        public string Firstname {get; set; }
        
        public string Lastname {get; set; }
        
        public DateTime Entered {get; set; }  = DateTime.Now;

        public Address Address {get; set;} = new Address();
        
        public bool Save()
        {
            // do whatever to save here
            return true;
        }

        public override string ToString()
        {
            return Firstname + &amp;quot; &amp;quot; + Lastname ?? string.Empty;
        }
    }

    public class Address
    {
        public string Street {get; set; }
        public string City {get; set; }
        public string State {get; set; }
        public string Zip  {get; set; }

        public override string ToString()
        {
            return Street + &amp;quot;\r\n&amp;quot; + City + &amp;quot;\r\n&amp;quot; + State + &amp;quot; &amp;quot; + Zip;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next we need to build the project. You can open a Terminal (Powershell here) either in Visual Studio Code or directly on the command line in the project folder and then build the project.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-ps"&gt;dotnet build FirstLibrary.csproj
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This builds the project into the output folder which happens to live below my FoxPro project folder as &lt;code&gt;,\bin&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I can then call the and the component from FoxPro with:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;CLEAR
do wwDotnetBridge  &amp;amp;&amp;amp; load lib
loBridge = GetwwDotnetBridge()

llResult = loBridge.LoadAssembly(&amp;quot;.\bin\FirstLibrary.dll&amp;quot;)
? loBridge.cErrorMsg  &amp;amp;&amp;amp; if there's an error and llResult is false

loPerson = loBridge.CreateInstance(&amp;quot;FirstLibrary.Person&amp;quot;)
loPerson.Firstname = &amp;quot;Rick&amp;quot;
loPerson.Lastname = &amp;quot;Strahl&amp;quot;
loPerson.Address = loBridge.CreateInstance(&amp;quot;FirstLibrary.Address&amp;quot;)
loPerson.Address.Street = &amp;quot;999 Emergency&amp;quot;
loPerson.Address.City = &amp;quot;AnyTown&amp;quot;
loPerson.Address.PostalCode = &amp;quot;97000&amp;quot;
loPerson.Entered = DATETIME()
loPerson.Save()

? loPerson.ToString()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Altogether it looks like this:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://west-wind.com/wconnect/weblog/imageContent/2025/CreatingAndDebuggingDotnetAssembliesForwwDotnetBridge/BuildingAndRunning.png" alt="Building And Running"&gt;&lt;/p&gt;
&lt;p&gt;The above does:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Builds the project and compiles into the app folder (....\bin)&lt;/li&gt;
&lt;li&gt;Open FoxPro&lt;/li&gt;
&lt;li&gt;Runs the test.prg file&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Cool it works.&lt;/p&gt;
&lt;h3 id="shutting-down-visual-foxpro-to-unload-net-assembly"&gt;Shutting down Visual FoxPro to Unload .NET Assembly&lt;/h3&gt;
&lt;p&gt;.NET and any components cannot be unloaded once loaded, so in order to update the DLL you have to shut down the host process, which in this case is Visual FoxPro (or your app EXE).&lt;/p&gt;
&lt;p&gt;If you need to make any changes to the DLL code, you need to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Shut down Visual FoxPro (or EXE)&lt;/li&gt;
&lt;li&gt;Rebuild the .NET project&lt;/li&gt;
&lt;li&gt;Restart Visual FoxPro&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To make this cycle a little easier I tend to create a small Powershell (or Command)  script that I can run from the terminal in the project folder:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-ps"&gt;dotnet build .\FirstLibrary.csproj

if ($LASTEXITCODE -ne 0) {
    exit
}

$startdir = $pwd

Set-Location ..\..
&amp;amp; D:\programs\vfp9\vfp9.exe
Set-Location $startdir
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This script handles building the project and launching VFP. It doesn't shut down FoxPro first - you'll have to do that manually, but building and starting up FoxPro is handled to make the launch a bit quicker than manually fumbling around to launch VFP in the right place.&lt;/p&gt;
&lt;p&gt;All of this works fine and it's pretty low impact so far. No need to install anything beyond the .NET SDK and VS Code (or your editor of choice).&lt;/p&gt;
&lt;p&gt;But things could be a little more integrated. And that's where Visual Studio comes in.&lt;/p&gt;
&lt;p&gt;If you want:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A smoother build and run cycle&lt;/li&gt;
&lt;li&gt;Ability to debug your DLL&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;then using Visual Studio is a worthwhile upgrade to the dev process.&lt;/p&gt;
&lt;h3 id="opening-the-project-in-visual-studio"&gt;Opening the Project in Visual Studio&lt;/h3&gt;
&lt;p&gt;Although I used Visual Studio Code (or no editor/IDE at all) to create the project originally, I can now also open this project in Visual Studio by opening the &lt;code&gt;FirstProject.csproj&lt;/code&gt; from Explorer and selecting &lt;em&gt;Visual Studio&lt;/em&gt; to open:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://west-wind.com/wconnect/weblog/imageContent/2025/CreatingAndDebuggingDotnetAssembliesForwwDotnetBridge/OpenProjectInVisualStudio.png" alt="Open Project In Visual Studio"&gt;&lt;br&gt;
&lt;small&gt;&lt;strong&gt;Figure 4&lt;/strong&gt; - Opening the project in Visual Studio lets you build and run more interactively&lt;/small&gt;&lt;/p&gt;
&lt;p&gt;From here you can build (right click &lt;em&gt;Build&lt;/em&gt; or &lt;em&gt;Rebuild&lt;/em&gt;) or you can &lt;em&gt;Debug&lt;/em&gt; the project with the debugger using the green run button.&lt;/p&gt;
&lt;p&gt;In order for that to work we need one more file to configure how to 'launch' our Dll. Create a &lt;code&gt;launchsettings.json&lt;/code&gt; file that contains the startup options (you can also do this from &lt;strong&gt;Properties ? Debug ? Open Debug Launch Profiles Ui&lt;/strong&gt; but it's easier to create the file manually):&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-json"&gt;{
  &amp;quot;profiles&amp;quot;: {
    &amp;quot;VFP (Debug)&amp;quot;: {
      &amp;quot;commandName&amp;quot;: &amp;quot;Executable&amp;quot;,
      &amp;quot;executablePath&amp;quot;: &amp;quot;d:\\programs\\vfp9\\vfp9.exe&amp;quot;,    
      &amp;quot;workingDirectory&amp;quot;: &amp;quot;D:\\wwapps\\Conf\\wwDotnetBridgeRevisited\\Dotnet\\FirstLibrary&amp;quot;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This file specifies the path to the Visual FoxPro IDE (or if you prefer your application executable) and the folder that you want to start it in, which is your FoxPro project folder. From there you can then run your application or test program as I did in the example above.&lt;/p&gt;
&lt;h3 id="debugging"&gt;Debugging&lt;/h3&gt;
&lt;p&gt;For debugging it's important to understand that you are driving the debugging process through Visual Studio/.NET rather than through your FoxPro application. Visual Studio launches FoxPro or your application, and you then run the code that eventually hits the .NET code that you have a breakpoint on. But Visual Studio is in control of the process and FoxPro is just the target that actually calls into .NET code you are debugging.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Note that you can debug your own code .NET readily enough, but in most cases you can't debug external, system or third party .NET code unless debug information (&lt;code&gt;.pdb&lt;/code&gt;) file is provided by the vendor.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;So, with the Launch profile configured we can now run the application in Debug mode. When running in Debug mode you can now set breakpoints in your code, and the debugger then stops on those breakpoints:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://west-wind.com/wconnect/weblog/imageContent/2025/CreatingAndDebuggingDotnetAssembliesForwwDotnetBridge/VisualStudioDebuggerBreakpoint.png" alt="Visual Studio Debugger Breakpoint"&gt;&lt;br&gt;
&lt;small&gt;&lt;strong&gt;Figure 5&lt;/strong&gt; - Stopped on a breakpoint in the Visual Studio Debugger. &lt;/small&gt;&lt;/p&gt;
&lt;p&gt;When on a breakpoint you can examine values using the Locals and Watch windows, but you can also hover over any active values or properties to display their values. From there you can continue to step over code using all the standard over, into, out step operations you're familiar with.&lt;/p&gt;
&lt;p&gt;Another nice feature: The debugger supports hot reload, which means you can make changes to some code while the app is running. If you've changed code that's already executed, you can move the execution pointer back to before the edit location and the code runs again with the newly compiled code. This can be a huge time saver when you're fixing code and trying to get it to work correctly.&lt;/p&gt;
&lt;p&gt;This example doesn't use any external libraries, but if you were interfacing with other libraries things work exactly the same. Again - debugging is limited to your own code in most cases.&lt;/p&gt;
&lt;p&gt;And that's it! With this you have all you need to build, run and debug your .NET components.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;VS Code also supports debugging and there's also a commandline debugger available. However, those debuggers only work with .NET Core and even then they don't work very well with an external process to attach to.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;##AD##&lt;/p&gt;
&lt;h2 id="summary"&gt;Summary&lt;/h2&gt;
&lt;p&gt;If you're using wwDotnetBridge it's a good idea to take advantage of .NET as best as you can by avoiding excessive .NET code written from FoxPro code using wwDotnetBridge functions. If you're writing copious amounts of wwDotnetBridge code in FoxPro it's time to think about offloading that code into a .NET class or component that you can abstract and call from FoxPro with a simpler and much less chatty interface.&lt;/p&gt;
&lt;p&gt;Building .NET libraries is much easier than it used to be in days past, either using the no-IDE command line tooling, or if you want the IDE experience with a somewhat less intrusive Visual Studio installation. The tooling is drastically better and less intrusive to install than it used to be a few years ago.&lt;/p&gt;
&lt;p&gt;By using .NET code for more complex tasks you can make development of your code easier, and also improve performance by avoiding a chatty COM interop interface calls for every member access. While performance of wwDotnetBridge both with native COM calls and Reflection intrinsic method calls, doing those calls directly in .NET is still considerably faster.&lt;/p&gt;
&lt;p&gt;I highly recommend this approach on anything that exceeds a few lines of .NET code that you need to call from FoxPro. .NET code is compact and you can cram many different components into a single tiny DLL that you can ship with your application with little to no overhead. And since you're using wwDotnetBridge already anyway having one extra DLL is not an issue.&lt;/p&gt;
&lt;p&gt;Finally, by offloading some processing into .NET you have a chance to learn something new at your own pace. Because wwDotnetBridge interfaces deal with components you can create components large and small to move small chunks of your application to .NET that you can perhaps use in the future in other projects or if you decide to migrate in the future.&lt;/p&gt;
&lt;h2 id="resources"&gt;Resources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/RickStrahl/wwDotnetBridge"&gt;wwDotnetBridge Github Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.west-wind.com/wconnect/weblog/ShowEntry.blog?id=57032"&gt;wwDotnetBridge Revisited Article&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
     </item>
     <item>
			<title>FoxPro Running on a Windows ARM Device</title>
			<pubDate>Thu, 7 Nov 2024 08:42:14 GMT</pubDate>
			<guid isPermaLink="false">57035_20241106</guid>
			<link>https://west-wind.com/wconnect/weblog/ShowEntry.blog?id=57035</link>
			<dc:creator>Rick Strahl</dc:creator>
			<comments>https://west-wind.com/wconnect/weblog/ShowEntry.blog?id=57035#Feedback</comments>
			<slash:comments>6</slash:comments>
			<description>&lt;p&gt;I recently picked up a Windows ARM device in the form of a &lt;a href="https://www.samsung.com/us/computing/galaxy-books/galaxy-book4-edge/buy/galaxy-book4-edge-14-qualcomm-snapdragon-x-elite-512gb-sapphire-blue-np940xma-kb1us/"&gt;Samsung Galaxy Book 4&lt;/a&gt; with a SnapDragon X Elite chip. Best Buy had a sale going for $799 at the time, and so I 'snapped' one up.&lt;/p&gt;
&lt;p&gt;I was pleasantly surprised that almost all of my Windows applications run without issues. This is true for both .NET and FoxPro applications as  far as my own apps are concerned, and just about anything else in my typical Windows application and tools collection.&lt;/p&gt;
&lt;p&gt;##AD##&lt;/p&gt;
&lt;h2 id="foxpro-and-web-connection"&gt;FoxPro and Web Connection&lt;/h2&gt;
&lt;p&gt;One of the things I was really curious about was whether FoxPro applications would work. After all FoxPro is a 32 bit application (ie. not x64) and it's based on 30 year old Windows code.&lt;/p&gt;
&lt;p&gt;In fact, to test out FoxPro operation under ARM I tried the following from scratch:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Install Visual FoxPro and SP2-HF3&lt;/li&gt;
&lt;li&gt;Copy over my Web Connection Dev folder from my main laptop&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Then proceeded to  fire up FoxPro in my dev folder and ran:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;Launch()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;which in my setup launches the Web Connection .NET Core Web Server.&lt;/p&gt;
&lt;p&gt;To my delight, &lt;strong&gt;everything fired right up and ran on the very first try&lt;/strong&gt; - without any changes whatsoever:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://support.west-wind.com/PostImages/2024/_MHkmIR1jOIS5yFki.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;&lt;small&gt;&lt;em&gt;Full disclosure: I did already have the &lt;a href="https://dotnet.microsoft.com/en-us/download/dotnet/8.0"&gt;.NET 8.0 SDK installed&lt;/a&gt; in order for the .NET Core server to run&lt;/em&gt;&lt;/small&gt;&lt;/p&gt;
&lt;p&gt;The Web Connection FoxPro server runs fine under ARM in x64 emulation mode, but surprisingly performance is very good. Since this is a Windows Home machine full IIS  is not an option, but I was able to set up IIS Express on this machine and it too &lt;strong&gt;just works&lt;/strong&gt; on the first try:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://west-wind.com/wconnect/weblog/imageContent/2025/FoxPro-Running-on-Windows-ARM-Device/IisExpressRunningOnArm.png" alt="Iis Express Running On Arm"&gt;&lt;/p&gt;
&lt;p&gt;IIS Express runs in x64 emulation but that's also not a problem and performance seems to be on par with the .NET Core version. That performance of emulation is almost on par with the native .NET Core server's ARM binary is almost more impressive than the .NET server running without any changes at all.&lt;/p&gt;
&lt;p&gt;Neither the native .NET Core server or the Web Connection Module under IIS Express feel laggy and request hits are in a the 5-10ms range which is a only little slower than what I see on my i9 development laptop (trending towards the higher end of the range on ARM vs. lower end on I9).&lt;/p&gt;
&lt;p&gt;Earlier also fired up West Wind Html Help Builder, which is a FoxPro desktop application running with a FoxPro runtime installation and it also worked without any issues. That app does a lot of oddball things with FoxPro UI, ActiveX controls, the WebBrowser control and native Win32 calls. Yet it just runs without any issues.&lt;/p&gt;
&lt;p&gt;Pretty cool!&lt;/p&gt;
&lt;h2 id="everything-works---except-sql-server"&gt;Everything works - except SQL Server!&lt;/h2&gt;
&lt;p&gt;When I picked this machine up and started installing my typical developer and productivity workload on it, I was expecting a lot of stuff to not work or work badly through emulation.  It turns out I was wrong! Mostly!&lt;/p&gt;
&lt;h3 id="just-about-everything-works"&gt;Just about everything works!&lt;/h3&gt;
&lt;p&gt;I've been pleasantly surprised in that just about everything works - either via many native ARM applications that are now becoming available, or via the built in and &lt;em&gt;'not bad at all'&lt;/em&gt; x64 emulation.&lt;/p&gt;
&lt;p&gt;There are a few things that don't work well - older versions of SnagIt for example don't want to capture system windows correctly and my audio software was having problems keeping the external audio interface connected. Mostly edge cases, but be aware there are somethings - especially hardware or low level windows stuff - that can have problems.&lt;/p&gt;
&lt;p&gt;The one let down is that performance is not quite as impressive as was &lt;del&gt;advertised&lt;/del&gt; hyped. It feels like there's quite a bit of lag, especially during application startup initially. Performance overall feels at best like a mid-level laptop, certainly not something that would replace a higher end developer I7/I9 laptop for me, which I had expected given the promotional hype. Even local AI operations using Ollama which is what these machines were supposed to be best at are only barely better for local AI processing compared to my I9 and low-midrange nVidia card equipped laptop do.&lt;/p&gt;
&lt;h3 id="net-applications---just-run-natively"&gt;.NET Applications - Just Run Natively!&lt;/h3&gt;
&lt;p&gt;What is quite impressive though is that all of my .NET apps, - including a several complex Windows Desktop applications - ran without even recompiling, natively under ARM64. &lt;a href="https://markdownmonster.west-wind.com/"&gt;Markdown Monster&lt;/a&gt; and &lt;a href="https://websurge.west-wind.com/"&gt;West Wind WebSurge&lt;/a&gt; both run &lt;strong&gt;without any changes&lt;/strong&gt; and I have yet to find any problems on ARM. All of my Web apps also 'just run' under ARM64 and I have yet to see any errors or operational differences. That's impressive - especially for the desktop apps which use a ton of P/Invoke and native code in addition to raw .NET code. And it... just... works!&lt;/p&gt;
&lt;h3 id="the-one-thing-that-was-problematic-sql-server"&gt;The one thing that was problematic: SQL Server&lt;/h3&gt;
&lt;p&gt;The only real issue I ran into was SQL Server, which does not have an installer from Microsoft that works with Windows ARM.&lt;/p&gt;
&lt;p&gt;There are two workarounds that &lt;a href="https://weblog.west-wind.com/posts/2024/Oct/24/Using-Sql-Server-on-Windows-ARM"&gt;I discuss in a separate post on my main blog&lt;/a&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Using LocalDb with Named Pipes&lt;/li&gt;
&lt;li&gt;Using a hacked installer to install SQL Express or SQL Developer&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you need to work with a local copy of SQL Server &lt;a href="https://weblog.west-wind.com/posts/2024/Oct/24/Using-Sql-Server-on-Windows-ARM"&gt;check out the post&lt;/a&gt; that goes into greater detail.&lt;/p&gt;
&lt;p&gt;##AD##&lt;/p&gt;
&lt;h2 id="summary"&gt;Summary&lt;/h2&gt;
&lt;p&gt;So, the good news is ARM devices have a lot going for it. There are now many native ARM applications available - most mainstream applications have ARM specific versions or can run under ARM. Most .NET applications can automatically run natively on ARM if they are compiled for &lt;code&gt;AnyCPU&lt;/code&gt; mode.&lt;/p&gt;
&lt;p&gt;For those apps that don't have native support x64 Emulation is surprisingly good and fairly fast. Frankly I'm not sure that I can really tell the difference from emulation to native - emulated apps seem a little slow to start up, but once running the emulation seems as fast as I would expect for this machine's processing power (ie. a mid-level business laptop).&lt;/p&gt;
&lt;p&gt;In summary ARM device compatibility is much better than I expected and performance is about on par what I thought it would be, but a bit below the expectations that were hyped up for ARM devices.&lt;/p&gt;
&lt;p&gt;All of this is great news for FoxPro it extends the lifetime of FoxPro just a little longer yet... who would have thunk it?&lt;/p&gt;
&lt;div style="margin-top: 30px;font-size: 0.8em;
            border-top: 1px solid #eee;padding-top: 8px;"&gt;
    &lt;img src="https://markdownmonster.west-wind.com/favicon.png" style="height: 20px;float: left; margin-right: 10px;"&gt;
    this post created and published with the 
    &lt;a href="https://markdownmonster.west-wind.com" target="top"&gt;Markdown Monster Editor&lt;/a&gt; 
&lt;/div&gt;
</description>
     </item>
     <item>
			<title>Web Connection 8.1 Release Post</title>
			<pubDate>Mon, 14 Oct 2024 22:29:02 GMT</pubDate>
			<guid isPermaLink="false">57034_20241014</guid>
			<link>https://west-wind.com/wconnect/weblog/ShowEntry.blog?id=57034</link>
			<dc:creator>Rick Strahl</dc:creator>
			<comments>https://west-wind.com/wconnect/weblog/ShowEntry.blog?id=57034#Feedback</comments>
			<slash:comments>0</slash:comments>
			<description>&lt;p&gt;&lt;img src="https://webconnection.west-wind.com/images/WebConnection_Code_Banner.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Hi all,&lt;/p&gt;
&lt;p&gt;Web Connection 8.1 has been released. This update is mostly a maintenance release that cleans up a few small bugs and issues, but it also introduces a few notable improvements:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Support for greater than 16mb Web Response output&lt;/li&gt;
&lt;li&gt;More improvements to COM server handling&lt;/li&gt;
&lt;li&gt;Improved wwDotnetBridge stability (no more crashes after full memory unloads)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Let's take a look.&lt;/p&gt;
&lt;h3 id="support-for-greater-than-16mb-web-response-output"&gt;Support for greater than 16mb Web Response Output&lt;/h3&gt;
&lt;p&gt;Back in the Web Connection 5.0 days Web Connection switched from file based output to string based output, which it turns out is a lot faster and more flexible than direct file based output. File based output was able to write output of any size as the file content can be written out to any size. String based output on the other hand is - potentially - subject to FoxPro's 16mb memory limit. In version 5.0 Web Connection introduced the wwPageResponse class which switched to an in memory buffer that builds up a string for output.&lt;/p&gt;
&lt;p&gt;Essentially it does:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;FUNCTION Write(lcText,llNoOutput)

IF !THIS.ResponseEnded
   THIS.cOutput = this.cOutput + lcText
ENDIF  

RETURN &amp;quot;&amp;quot;
ENDFUNC
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;FoxPro 9.0 is &lt;strong&gt;extremely efficient&lt;/strong&gt; at string concatenation and at the time this provided a large boost in performance - especially related to the complexities of the then introduced Web Connection Web Control Framework (now deprecated) which relied on tons and tons for small components to write small bits of code.&lt;/p&gt;
&lt;p&gt;But... with that change came the limitation related to FoxPro's 16mb string limit. Ironically the way Web Connection's response class was designed - unintentionally I might add - the above code for string concatenation actually works just fine &lt;strong&gt;even for strings larger than 16mb&lt;/strong&gt;. Unfortunately, the code would fail later on when writing out the &lt;strong&gt;entire response including the headers&lt;/strong&gt; which required an incompatible string operation.&lt;/p&gt;
&lt;p&gt;Specifically it's this code:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;*** This fails if cOutput &amp;gt; 16mb
RETURN this.RenderHttpHeader() + THIS.cOutput
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is one of the quirks of the 16mb string limit in FoxPro: You can actually create larger strings, but you cannot concatenate or send a greater than 16mb string to another operation that modifies the string. Essentially that code above in the &lt;code&gt;wwPageResponse::Render()&lt;/code&gt; method broke the code.&lt;/p&gt;
&lt;p&gt;I wrote in more detail about the workaround to this issue in a previous Blog Post here if you're interested:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://west-wind.com/wconnect/weblog/ShowEntry.blog?id=57033"&gt;Making Web Connection Work with Response Output Greater than 16mb&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The post includes the actual changes in Web Connection along with the code work around and the helpers - you might find those helpers useful in your own code that has to deal with large strings.&lt;/p&gt;
&lt;p&gt;The key bit of code that uses the updated string processing is in &lt;code&gt;wwProcess.CompleteResponse()&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;LOCAL lcResponse, lcHttp, lnX, loCol

*** Grab the response content
lcResponse = this.oResponse.Render(.T.)

*** IMPORTANT: Must follow the Render() as Render() may add headers (GZip/Utf8 etc)
lcHttp = this.oResponse.RenderHttpHeader() 

*** Check for 16mb+ output size
IF LEN(lcResponse) &amp;gt; 15800000    
   loCol = SplitStringBySize(lcResponse,5500000)   
   lcResponse = &amp;quot;&amp;quot;
   FOR lnX = 1 TO loCol.Count
       lcHttp = lcHttp + loCol.Item(lnX)
   ENDFOR
   loCol = null
ELSE    	
   lcHttp = lcHttp + lcResponse
   lcResponse = &amp;quot;&amp;quot;
ENDIF

IF THIS.oServer.lComObject
  *** Assign text output direct to COM Server output
  THIS.oServer.cOutput= lcHttp
ELSE
  FILE2VAR(this.oRequest.GetOutputFile(),lcHttp)
ENDIF
lcHttp = &amp;quot;&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In a nutshell, the code change involves breaking up the large string into a collection of smaller strings, and then building a new string that adds the headers first and then appends the all of the string chunks which now allows for &amp;gt;16mb for both headers and content.&lt;/p&gt;
&lt;p&gt;This way there's never a &amp;gt;16mb string on the update side or right side of &lt;code&gt;=&lt;/code&gt; assignment operation and so we can write strings of any size, memory permitting (and yes you &lt;strong&gt;can&lt;/strong&gt; run out of memory).  It's a hack and ends up costing some extra memory overhead as strings are duplicated, but it works surprisingly well and it now allows us to return very large HTTP responses in memory.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;When working with large strings like this, you'll want to clear any in progress strings and the collection as soon as you no longer need it to avoid holding on to memory any longer than you have to.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;To be clear Web Connection has had support for more than 16mb output via file operations - using &lt;code&gt;Response.DownloadFile()&lt;/code&gt; and &lt;code&gt;Response.TransferFile()&lt;/code&gt; which can serve content of any size. But it's a bit more complicated to generate output to file first and then be able to delete the content after it's been sent.&lt;br&gt;
&lt;em&gt;(I'm thinking of another enhancement to allow for file deletion after sending in a future update)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Another related hack for large HTTP text results (HTML, JSON etc.)  is to use &lt;code&gt;Response.GzipCompression = .T.&lt;/code&gt;  which reduces the size of the HTTP output before headers are even added. Especially in the case of JSON compression can often knock down the size of a JSON document by more 5x since there's a lot of repetitive data in JSON or XML documents.  There's more info in the above &lt;a href="https://west-wind.com/wconnect/weblog/ShowEntry.blog?id=57033"&gt;blog post&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id="related-wwpageresponse-and-wwutils-changes"&gt;Related wwPageResponse and wwUtils Changes&lt;/h4&gt;
&lt;p&gt;There are a couple wwPageResponse class changes that are related to the above changes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A new wwPageResponse::WriteFullResponse(lcData)&lt;/strong&gt;
This method writes out a full response by overwriting any existing data. Added to both wwPageResponse, and wwResponse.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;wwPageResponse.Render(llOutputOnly)&lt;/strong&gt;&lt;br&gt;
The &lt;code&gt;Render()&lt;/code&gt; method has a new &lt;code&gt;llOutput&lt;/code&gt; parameter that returns only the response &lt;strong&gt;content&lt;/strong&gt; without headers. &lt;code&gt;Render()&lt;/code&gt; by default returns both headers and content and this override is used when creating the final output that's sent back to the Web server in file or COM based output.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;SplitStringBySize() in wwutils.prg&lt;/strong&gt;&lt;br&gt;
This splits a string into a collection string chunks. This function is used to split out the &amp;gt;16mb content into smaller strings before being reassembled into a single string including the header.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;JoinString() in wwutils.prg&lt;/strong&gt;&lt;br&gt;
The opposite of the above - takes strings in a collection and returns a single string.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="improvements-to-com-server-handling"&gt;Improvements to COM Server Handling&lt;/h3&gt;
&lt;p&gt;Web Connection 8.0 has introduced improved COM Server loading, which drastically speeds up COM server loading and provides a lot more checking deterministic termination of servers that cannot be shut down via COM either because they are still busy or have been otherwise orphaned. In the past this has been an ongoing problem for those that were running large numbers of COM Server instances as it took a) very long to load many instances and b) could result in orphaned server instances that would never be shut down - ie. a lot of &lt;strong&gt;extra&lt;/strong&gt; EXEs in task manager.&lt;/p&gt;
&lt;p&gt;v8.0 addressed both of these issues with parallel loading of COM servers, immediate server startup while others are still loading and a more robust shutdown sequence that tries to shut down all instances of a given executable as opposed of just the actual process ids of servers that are being shut down.&lt;/p&gt;
&lt;p&gt;It took a bit of experimenting during the beta of v8.0 to get this right - many thanks to Richard Kaye during the original beta, and Scott Rindlsbacher during the v8.1 cycle, who kindly both were willing to test with large production and staging environments.&lt;/p&gt;
&lt;p&gt;In v8.1 we identified an outlier issue that caused issues with COM server loading for large server pools due to a hard coded timeout that was causing the server pool to not complete loading. This would result in the server load cycle to error out and then try to reload again, going through a rather unpleasant re-cycling loop.&lt;/p&gt;
&lt;p&gt;In this update I've made the server load timeout configurable and allow for a larger timeout to begin with. So this is much less likely to run up into the timeout in the first place and if it still occurs can be adjusted with a larger timeout.&lt;/p&gt;
&lt;p&gt;The new timeout logic allows for 2 seconds per server to load. That should be plenty, plus parallel loading should reduce the overall time significantly. So if you have 10 servers the load timeout is 20 seconds. If it really takes that long to load servers - there's probably something wrong with the server, as it should not be that slow to load ??&lt;/p&gt;
&lt;p&gt;For reference the original release had a max timeout of 5 seconds with 1 second load time per server. I had figured that this would be enough given that parallel loading would ensure that servers load somewhere around maybe 2x their individual load time. But it turns out that if you run in a processor constricted environment that is not uncommon on virtual VPC machines you may still end up loading sequentially. For this reason I removed the max value and stick with the per server load timeout that is configurable.&lt;/p&gt;
&lt;p&gt;The new value is in configuration in web.config &lt;em&gt;(Web Connection .NET Module)&lt;/em&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-xml"&gt;&amp;lt;add key=&amp;quot;ComServerPerServerLoadTimeoutMs&amp;quot; value=&amp;quot;2000&amp;quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or in &lt;code&gt;WebConnectionServerSettings.xml&lt;/code&gt; &lt;em&gt;(.NET Core Web Connection Web Server)&lt;/em&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-xml"&gt;&amp;lt;ComServerPerServerLoadTimeoutMs&amp;gt;2000&amp;lt;/ComServerPerServerLoadTimeoutMs&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="improved-wwdotnetcorebridge-stability"&gt;Improved wwDotnetCoreBridge Stability&lt;/h3&gt;
&lt;p&gt;In relation to my &lt;a href="https://west-wind.com/wconnect/weblog/ShowEntry.blog?id=57032"&gt;wwDotnetBridge talk at Southwest Fox&lt;/a&gt; I did a bunch of work surrounding wwDotnetBridge.&lt;/p&gt;
&lt;p&gt;One issue that's been coming up a few times has been that the new &lt;code&gt;wwDotnetCoreBridge&lt;/code&gt; class has been somewhat unstable. In previous releases the library worked fine, but it would fail if FoxPro does a full memory reset via &lt;code&gt;CLEAR ALL&lt;/code&gt; or &lt;code&gt;RELEASE ALL&lt;/code&gt; etc. What was happening is that unlike the full framework version of wwDotnetBridge, the .NET Core version would clear out all of its memory as the native Windows interface would not pin the loader code in memory.&lt;/p&gt;
&lt;p&gt;Thanks to an &lt;a href="https://github.com/RickStrahl/wwDotnetBridge/issues/30"&gt;issue report and some great sleuthing&lt;/a&gt; by @horch004 who found the workaround by pinning the loader DLL into memory and thereby ensuring that the memory would not be wiped a FoxPro memory clearing operation.&lt;/p&gt;
&lt;p&gt;The fix for the Core is to pin the DLL and then simply return the already loaded .NET wwDotnetBridge instance and that works great - no more crashes after a full memory wipe.&lt;/p&gt;
&lt;h3 id="breaking-change-updated-log-format"&gt;Breaking Change: Updated Log Format&lt;/h3&gt;
&lt;p&gt;In this release the Web Connection log format has been updated for two things:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Widened the RemoteAddr field to support ipV6 addresses&lt;/li&gt;
&lt;li&gt;Added a Size field to log the response size  (if response is captured)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To deal with this you need to update the &lt;code&gt;wwRequestLog&lt;/code&gt; FoxPro table or the corresponding SQL table.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;For FoxPro&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Delete the &lt;code&gt;wwRequestLog.dbf&lt;/code&gt; table. It'll be recreated with the new file structure when Web Connection re-starts.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;For Sql Server&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Update the table using SQL commands or manually in your SQL Admin tool &lt;em&gt;(changed fields separated below)&lt;/em&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sql"&gt;CREATE TABLE [dbo].[wwrequestlog](
	[time] [datetime] NOT NULL DEFAULT getdate(),
	[reqid] [varchar](25) NOT NULL DEFAULT '',
	[script] [varchar](50) NOT NULL DEFAULT '',
	[querystr] [varchar](1024) NOT NULL DEFAULT '',
	[verb] [varchar](10) NOT NULL DEFAULT '',
	[duration] [numeric](7, 3) NOT NULL DEFAULT 0,
	
	[remoteaddr] [varchar](40) NOT NULL DEFAULT '',
	
	[memused] [varchar](15) NOT NULL DEFAULT '',
	[error] [bit] NOT NULL DEFAULT 0,
	[reqdata] [text] NOT NULL DEFAULT '',
	[servervars] [text] NOT NULL DEFAULT '',
	[browser] [varchar](255) NOT NULL DEFAULT '',
	[referrer] [varchar](255) NOT NULL DEFAULT '',
	[result] [text] NOT NULL DEFAULT '',
	
	[size] [int] NOT NULL DEFAULT 0,
	
	[account] [varchar](50) NOT NULL DEFAULT ''
) ON [PRIMARY]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="summary"&gt;Summary&lt;/h3&gt;
&lt;p&gt;There you have it. Web Connection 8.1 is a minor update and other than the logging table changes for SQL there are no breaking changes. As always you can update the project by checking out the update topic in the documentation.&lt;/p&gt;
&lt;p&gt;If you run into any issues please post a message on the message board:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://support.west-wind.com"&gt;Web Connection Support&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div style="margin-top: 30px;font-size: 0.8em;
            border-top: 1px solid #eee;padding-top: 8px;"&gt;
    &lt;img src="https://markdownmonster.west-wind.com/favicon.png" style="height: 20px;float: left; margin-right: 10px;"&gt;
    this post created and published with the 
    &lt;a href="https://markdownmonster.west-wind.com" target="top"&gt;Markdown Monster Editor&lt;/a&gt; 
&lt;/div&gt;
</description>
     </item>
     <item>
			<title>Making Web Connection Work with Response Output Greater than 16mb</title>
			<pubDate>Tue, 1 Oct 2024 01:30:22 GMT</pubDate>
			<guid isPermaLink="false">57033_20240930</guid>
			<link>https://west-wind.com/wconnect/weblog/ShowEntry.blog?id=57033</link>
			<dc:creator>Rick Strahl</dc:creator>
			<comments>https://west-wind.com/wconnect/weblog/ShowEntry.blog?id=57033#Feedback</comments>
			<slash:comments>1</slash:comments>
			<description>&lt;p&gt;&lt;img src="https://west-wind.com/wconnect/weblog/imageContent/2025/Making-Web-Connection-Work-with-Response-Output-Greater-than-16mb/StringLimit.jpg" alt="String Limit"&gt;&lt;/p&gt;
&lt;p&gt;During this year's Southwest Fox conference and Scott Rindlisbacher's session, he was discussing generating &lt;strong&gt;very large JSON output&lt;/strong&gt; using &lt;a href="https://webconnection.west-wind.com/"&gt;West Wind Web Connection&lt;/a&gt; with output that exceeds 16mb as part of his application using REST JSON services. The issues raised where two-fold:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Output over 16mb would fail&lt;/li&gt;
&lt;li&gt;Large output was too slow to travel to the client&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;My first thought was that I had addressed the &amp;gt;16mb issue previously, but after some initial testing it's clear that &lt;strong&gt;that was not the case!&lt;/strong&gt; &lt;a href="https://webconnection.west-wind.com/"&gt;Web Connection&lt;/a&gt; in the past has been limited to 16mb output due to FoxPro's 16mb string limit, for direct &lt;code&gt;Response&lt;/code&gt; output. Although there are ways to send larger output via files, that can be a lot more complicated.&lt;/p&gt;
&lt;p&gt;During the conference I spent some time to create a solution to this problem and Web Connection can now serve content &amp;gt;16mb. If you're on Web Connection 8.0 and you want to experiment with this updated support, you can pick up an experimental update here:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://west-wind.com/files/WebConnectionExperimental.zip"&gt;Web Connection Experimental (v8.1)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These features will be shipped with the next release in Web Connection v8.1.&lt;/p&gt;
&lt;p&gt;If you're interested, what follows is a discussion on how I worked around this limitation along with a review of how you can create string content that exceeds 16mb in FoxPro in general.&lt;/p&gt;
&lt;p&gt;##AD##&lt;/p&gt;
&lt;h2 id="making-16mb-output-work-in-web-connection"&gt;Making &amp;gt;16mb Output work in Web Connection&lt;/h2&gt;
&lt;p&gt;So while at SW Fox I started playing around with some ideas on how to make &amp;gt;16mb content work, and implemented a solution to this problem.&lt;/p&gt;
&lt;p&gt;The solution hinges around intercepting output that is returned and ensuring that no parts of the output strings that are being sent are &amp;gt;16mb, and if they are splitting up those strings into smaller chunks that can be concatenated.&lt;/p&gt;
&lt;p&gt;If you want to learn more &lt;a href="https://west-wind.com/files/WebConnectionExperimental.zip"&gt;about how you can use &amp;gt;16mb strings in FoxPro, you can check out my post&lt;/a&gt; from a few years ago that shows how this can work if you are careful in how you assign large values.&lt;/p&gt;
&lt;blockquote&gt;
&lt;h4 id="--working-with-16mb-strings"&gt;&lt;i class="fas fa-lightbulb" style="font-size: 1.1em"&gt;&lt;/i&gt;  Working with &amp;gt;16mb Strings&lt;/h4&gt;
&lt;p&gt;You can assign &amp;gt;16mb by doing something like &lt;code&gt;lcOutput = lcOutput + lcContent&lt;/code&gt;, but no part of a string operation that &lt;strong&gt;manipulates a string and updates&lt;/strong&gt; can be larger than 16mb.&lt;/p&gt;
&lt;p&gt;The left side of the &lt;code&gt;=&lt;/code&gt; can become larger than 16mb, but the right side can never be greater than 16mb.&lt;/p&gt;
&lt;p&gt;You also cannot call functions that changes the value of a &lt;code&gt;&amp;gt;16mb&lt;/code&gt; string but some functions &lt;strong&gt;can return&lt;/strong&gt; a greater than 16mb string.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Web Connection is already build on incremental building of a string in memory by doing effectively:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;FUNCTION Write(lcData)
THIS.cOutput = this.cOutput + lcData
ENDFUNC
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;which allows writing output &amp;gt;16mb &lt;strong&gt;as long lcData&lt;/strong&gt; is smaller than 16mb. Most of Web Connection's implementation features run through the &lt;code&gt;Response.Write()&lt;/code&gt; or friends methods, including complex methods like &lt;code&gt;Response.ExpandScript()&lt;/code&gt; and REST Process class JSON output, so as long as no individual &lt;code&gt;Response.Write()&lt;/code&gt; operation writes data larger than 16mb, all output automatically can be larger than 16mb.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;wwJsonSerializer&lt;/code&gt; already supported &amp;gt;16mb JSON output, and with the changes I added this week any response larger than 16mb is chunked into the actual Response output (so there the content effectively gets chunked twice - once for the JSON and once for header/httpoutput).&lt;/p&gt;
&lt;p&gt;The issue with the actual HTTP output is that Web Connection pre-pends the HTTP headers in front of the HTTP response. The HTTP response can be larger than 16mb, but if we &lt;strong&gt;prepend the headers&lt;/strong&gt; in front of a 16mb string - that doesn't work (ie. right side  of &lt;code&gt;=&lt;/code&gt; &amp;gt;16mb).&lt;/p&gt;
&lt;p&gt;The workaround for this is: Chunking the string into smaller block that can be written (5mb chunks).&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;lcHttp = Response.RenderHttpHeader()
lcHttp = lcHttp + Response.GetOutput()  &amp;amp;&amp;amp; This fails if the response is &amp;gt;16mb
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To fix this I ended up creating a small helper function that splits strings by size to take the string that is larger than 16 megs and splitting it into chunks.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;*** Inside of wwProcess::CompleteResponse()
LOCAL lcResponse, lcHttp, lnX, loCol
lcResponse = this.oResponse.Render(.T.)

*** IMPORTANT: Must follow the Render as it may add headers
lcHttp = this.oResponse.RenderHttpHeader()

IF LEN(lcResponse) &amp;gt; 15800000    
   loCol = SplitStringBySize(lcResponse,5000000)  
   FOR lnX = 1 TO loCol.Count
       lcHttp = lcHttp + loCol.Item(lnX)
   ENDFOR
ELSE    	
   lcHttp = lcHttp + lcResponse
ENDIF

IF THIS.oServer.lComObject
  *** Assign text output direct to Server output
  THIS.oServer.cOutput= lcHttp                     &amp;amp;&amp;amp; can be &amp;gt;16mb
ELSE
  FILE2VAR(this.oRequest.GetOutputFile(),lcHttp)   &amp;amp;&amp;amp; can be &amp;gt;16mb
ENDIF
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Every request in Web Connection runs through this method so this single point of output and so any large output can be chunked.&lt;/p&gt;
&lt;p&gt;This uses a new helper function called &lt;code&gt;SplitStringBySize()&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;************************************************************************
*  SplitStringBySize
****************************************
***  Function: Splits a string into a collection based on size
***    Assume: Useful for breaking 16mb strings
***      Pass: lcString  - string to split
***            lnLength  - where to split
***    Return: Collection of strings or empty collection
************************************************************************
FUNCTION SplitStringBySize(lcString, lnLength)
LOCAL lnTotalLength, lnStart, lnEnd, loCollection

lnTotalLength = LEN(lcString)
loCollection = CREATEOBJECT(&amp;quot;Collection&amp;quot;)

FOR lnStart = 1 TO lnTotalLength STEP lnLength
    lnEnd = MIN(lnStart + lnLength - 1, lnTotalLength)
    loCollection.ADD(SUBSTR(lcString, lnStart, lnEnd - lnStart + 1))
ENDFOR

RETURN loCollection
ENDFUNC
*   SplitString
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As it turns out this works for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Plain Response.Write() requests (ie. hand coded)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Response.ExpandScript()&lt;/code&gt; and &lt;code&gt;Response.ExpandTemplate()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Any &lt;code&gt;wwRestProcess&lt;/code&gt; handler&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="use-16mb-content-with-care"&gt;Use &amp;gt;16mb Content with Care&lt;/h3&gt;
&lt;p&gt;Turns out this is a very useful addition, even if I would highly recommend that you don't do this often! It's a bad idea to send huge amounts of data back to the client as it is slow to send/receive, and if you're sending JSON data or HTML table data it'll take forever to parse or render. It also puts a lot of memory pressure on the Visual FoxPro application, and may result in out of memory errors if output is too large and isn't immediately cleaning up.&lt;/p&gt;
&lt;h2 id="reducing-output-size-significantly-with-gzipcompression"&gt;Reducing Output Size Significantly with GZipCompression&lt;/h2&gt;
&lt;p&gt;Another thing that you can and should do if you are returning large amounts of text data is enable &lt;code&gt;Response.GZipCompression = .T.&lt;/code&gt; either on the active request, or on all requests.&lt;/p&gt;
&lt;p&gt;You can do this either in:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Process.OnProcessInit&lt;/strong&gt;&lt;br&gt;
this will be global and applied to every request in this process class. Add to the &lt;code&gt;MyProcess::OnProcessInit()&lt;/code&gt; method.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Individual Process Method&lt;/strong&gt;&lt;br&gt;
this is specific to each individual method and applied only. Add to any Process method before the method completes.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Note that GZip compression only kicks in after output reaches a certain pre-configured size which is configured in &lt;code&gt;wconnect.h&lt;/code&gt;  and you can override it in &lt;code&gt;wconnect_override.h&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;#UNDEFINE GZIP_MIN_COMPRESSION_SIZE
#DEFINE GZIP_MIN_COMPRESSION_SIZE				15000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Anything smaller just gets returned un-encoded, while anything bigger gets GZipped.&lt;/p&gt;
&lt;p&gt;Web Connection supports GZip compression of content with just that flag and especially for repeating content like JSON you can cut the size of a document immensely - typical between 5x and 10x smaller than the original size. Using GZipCompression gives you literally a ton more breathing room before you bump up against the 16mb limit.&lt;/p&gt;
&lt;p&gt;In the testing I did a bit ago with a huge data set the data went from 18mb down to 2mb with GZipCompression. You get the benefit of a smaller memory footprint, plus vastly reduced transfer time of the data over the wire due to the smaller size.&lt;/p&gt;
&lt;p&gt;##AD##&lt;/p&gt;
&lt;h2 id="summary"&gt;Summary&lt;/h2&gt;
&lt;p&gt;16mb for text or even JSON output should be avoid as much as possible. 16mb is a lot of data to either render, or parse as JSON data and I would not recommend doing that under most circumstances. But I know some of you will do it anyway, so this is why we're here ??&lt;/p&gt;
&lt;p&gt;So, I've implemented this functionality in the current experimental update for Web Connection v8 so you can play with this right away.&lt;/p&gt;
&lt;p&gt;Additionally you can also minimize the need for hitting the 16mb limit in many cases by using &lt;code&gt;Response.GZipCompression = .t.&lt;/code&gt; which compresses the Response output. With typical HTML and JSON output, compression is significant with at least a 3x and as much as 10x reduction in output size in many instances. It's a very quick fix to reduce both the output size you're sending and keeping it under 16mb in the first place as well as reducing the network traffic and bandwidth usage significantly.&lt;/p&gt;
&lt;div style="margin-top: 30px;font-size: 0.8em;
            border-top: 1px solid #eee;padding-top: 8px;"&gt;
    &lt;img src="https://markdownmonster.west-wind.com/favicon.png" style="height: 20px;float: left; margin-right: 10px;"&gt;
    this post created and published with the 
    &lt;a href="https://markdownmonster.west-wind.com" target="top"&gt;Markdown Monster Editor&lt;/a&gt; 
&lt;/div&gt;
</description>
     </item>
     <item>
			<title>wwDotnetBridge Revisited: An updated look at FoxPro .NET Interop</title>
			<pubDate>Mon, 23 Sep 2024 07:34:21 GMT</pubDate>
			<guid isPermaLink="false">57032_20240922</guid>
			<link>https://west-wind.com/wconnect/weblog/ShowEntry.blog?id=57032</link>
			<dc:creator>Rick Strahl</dc:creator>
			<comments>https://west-wind.com/wconnect/weblog/ShowEntry.blog?id=57032#Feedback</comments>
			<slash:comments>0</slash:comments>
			<description>&lt;p&gt;&lt;img src="https://west-wind.com/wconnect/weblog/imageContent/2025/White-Paper/Bridge.jpg" alt="Bridge"&gt;&lt;/p&gt;
&lt;div style="margin: 1em 0 3em 0; font-size: 0.8em;"&gt;
&lt;p&gt;&lt;em&gt;by &lt;strong&gt;Rick Strahl&lt;/strong&gt;&lt;/em&gt;&lt;br&gt;
&lt;em&gt;prepared for &lt;strong&gt;Southwest Fox 2024&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/RickStrahl/swfox2024-wwdotnetbridge-revisited"&gt;Session Example Code&lt;/a&gt; on GitHub&lt;br&gt;
&lt;a href="https://github.com/RickStrahl/swfox2024-wwdotnetbridge-revisited/raw/master/Documents/Strahl-swFox2024-wwDotnetBridge-Revisited.pptx"&gt;Session Slides&lt;/a&gt;&lt;br&gt;
&lt;a href="https://github.com/RickStrahl/wwDotnetBridge"&gt;wwDotnetBridge Repo&lt;/a&gt; on GitHub&lt;br&gt;
&lt;a href="https://webconnection.west-wind.com/docs/_24n1cfw3a.htm"&gt;wwDotnetBridge Docs&lt;/a&gt;&lt;br&gt;
&lt;a href="https://support.west-wind.com"&gt;West Wind Message Board Support&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;.NET has proliferated as the dominant Windows development environment, both for Windows application development using a variety of different Windows-specific platforms and as the high-level API surface chosen by Microsoft to expose Windows system functionality besides low-level, native C++.&lt;/p&gt;
&lt;p&gt;More importantly though, .NET has garnered a huge eco system of open source and commercial libraries and components that provide just about any kind of functionality and integration you can think of.&lt;/p&gt;
&lt;p&gt;All of this is good news for FoxPro developers, as you can take advantage of most of that .NET functionality to &lt;strong&gt;extend your own FoxPro applications&lt;/strong&gt; with rich functionality beyond FoxPro's native features using either basic COM interop (very limited) or more usefully with the open source &lt;a href="https://github.com/RickStrahl/wwDotnetBridge"&gt;wwDotnetBridge library&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;##AD##&lt;/p&gt;
&lt;!-- Start Document Outline --&gt;
&lt;h2 id="what-we-cover"&gt;What we cover&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#net-history-net-framework-to-net-core"&gt;.NET History: .NET Framework to .NET Core&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#net-and-foxpro"&gt;.NET and FoxPro&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#why-net-for-foxpro"&gt;Why .NET for FoxPro?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#what-is-wwdotnetbridge"&gt;What is wwDotnetBridge?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#a-quick-primer"&gt;A quick Primer&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#setting-up-wwdotnetbridge"&gt;Setting up wwDotnetBridge&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#loading-wwdotnetbridge-and-loading-your-first-net-library"&gt;Loading wwDotnetBridge and Loading your first .NET Library&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#loading-assemblies-and-creating-an-instance"&gt;Loading Assemblies and Creating an Instance&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#direct-invocation-of-members"&gt;Direct Invocation of Members&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#proxy-invocation-for-problem-types-and-helpers"&gt;Proxy Invocation for Problem Types and Helpers&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#how-does-wwdotnetbridge-work"&gt;How does wwDotnetBridge Work&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#a-net-loader"&gt;A .NET Loader&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#wwdotnetbridge-loader"&gt;wwDotnetBridge Loader&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#direct-member-access-is-via-com"&gt;Direct Member Access is via COM&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#indirect-member-access-via-invokemethod-getproperty-and-setproperty"&gt;Indirect Member Access via InvokeMethod(), GetProperty() and SetProperty()&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#wwdotnetbridge-proxy-type-wrappers-comarray-and-comvalue"&gt;wwDotnetBridge Proxy Type Wrappers: ComArray and ComValue&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#figuring-out-whats-available-in-net"&gt;Working with .NET From FoxPro: Finding what you need!&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#figuring-out-what-assemblies-dlls-to-provide"&gt;Figuring out what Assemblies (DLLs) to Provide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#finding-dlls-unpacking-nuget-packages-and-their-dependencies-using-a-net-project"&gt;Finding DLLs: Unpacking NuGet Packages and their Dependencies using a .NET Project&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#use-a-disassembler-tool-to-discover-net-types-and-members"&gt;Use a Disassembler Tool to discover .NET Types and Members&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#use-linqpad-to-test-out-code"&gt;Use LinqPad to Test Out Code&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#usage-examples"&gt;10 Usage Examples&lt;/a&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href="#wwdotnetbridge-101--load-create-invoke-getset"&gt;wwDotnetBridge 101 â€“ Load, Create, Invoke, Get/Set&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#create-a-powerful-string-formatter"&gt;Create a powerful String Formatter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#add-markdown-parsing-to-your-applications"&gt;Add Markdown Parsing to your Applications&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#use-a-two-factor-authenticator-library"&gt;Use a Two-Factor Authenticator Library&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#add-spellchecking-to-your-applications"&gt;Add Spellchecking to your applications&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#humanize-numbers-dates-measurements"&gt;Humanize numbers, dates, measurements&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#a-file-system-watcher-and-live-reload-event-handling"&gt;A File System Watcher and Live Reload (Event Handling)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#async-print-html-to-pdf"&gt;Async: Print Html to Pdf&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#async-openai-calls"&gt;Async: OpenAI Calls&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;Language Translation&lt;/li&gt;
&lt;li&gt;Document Summary&lt;/li&gt;
&lt;li&gt;Image Generation&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#create-a-net-component-and-call-it-from-foxpro"&gt;Create a .NET Component and call it from FoxPro&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#configuration-and-application-management"&gt;Configuration and Application Management&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#net-configuration-config-files"&gt;.NET Configuration: .config Files&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#net-dependency-version-management"&gt;.NET Dependency Version Management&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#keep-it-simple-use-net-when-wwdotnetbridge-is-a-lot-of-effort"&gt;Keep it simple: Use .NET When wwDotnetBridge is a lot of Effort&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;!-- End Document Outline --&gt;
&lt;h2 id="net-history-net-framework-to-net-core"&gt;.NET History: .NET Framework to .NET Core&lt;/h2&gt;
&lt;p&gt;.NET has been around since the early 2000's and in those nearly 25 years it has undergone a number of transformations. From its early days as a limited distributed runtime, to integration into Windows as a core Windows component, to the splitting off of .NET Core as a cross platform capable version of .NET, to full cross-platform support for .NET Core, to recent releases that provide nearly full compatibility with .NET Framework for .NET Core including of Windows specific platforms (ie. WinForms, WPF, WinUI).&lt;/p&gt;
&lt;p&gt;The most significant change occurred in 2016, when .NET split off into the classic &lt;strong&gt;.NET Framework&lt;/strong&gt; (the Windows native Runtime build into Windows) and &lt;strong&gt;.NET Core&lt;/strong&gt;, which is a newly redesigned version of .NET that is fully cross-platform enabled and can run on Windows, Mac and Linux and that is optimized for performance and optimized resource usage. The new version has greatly expanded .NET's usefulness and developer reach with many new developers using the platform now.&lt;/p&gt;
&lt;p&gt;This new version of .NET - although it had a rough initial start - is also mostly compatible with the classic .NET Framework and can for the most part run code on both frameworks interchangeably. .NET Core brought a ton of improvements to .NET in terms of performance and resource usage, as well as new server side frameworks (ASP.NET, Blazor, Maui etc.),and much simpler and universally available tooling that removed the requirement for developing applications exclusively on Windows using Visual Studio.&lt;/p&gt;
&lt;p&gt;Today you can build applications for Windows, Mac or Linux, developing applications on these platforms using native editors either with integration tooling or command line tools that are freely available via the .NET SDK. The SDK includes all the compiler tools to build, run and publish .NET applications from the command line without any specific tooling requirements.&lt;/p&gt;
&lt;h2 id="net-and-foxpro"&gt;.NET and FoxPro&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;For FoxPro developers it's preferable to use components that use the old 'full framework' libraries&lt;/strong&gt; when available even though .NET Core is the &lt;em&gt;new and shiny&lt;/em&gt; new framework. The full .NET Framework (NetFX) is part of Windows and so it's always available - &lt;strong&gt;there's nothing else to install&lt;/strong&gt; to run it and so it's the easiest integration path for FoxPro applications. For this reason I strongly recommend you use .NET Framework in lieu of .NET Core or .NET Standard components &lt;em&gt;if possible&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;However, it is possible to use .NET Core components with FoxPro and wwDotnetBridge. But the process of doing so tends to be more complicated as .NET Core's assembly loading is more complex, often requiring many more support assemblies that are not always easy to identify.&lt;/p&gt;
&lt;p&gt;In order to use .NET Core you need to ensure a matching .NET Core runtime is installed to support the minimum version of any components you are calling. .NET Core goes back to requiring installable runtimes, rather than having a single system provided runtime as .NET Framework does. This means you have to ensure the right version of the runtime is installed. Although .NET Core also supports fully self contained installs, that's not really applicable to components or FoxPro applications, so we're always dependent on an installed runtime. Major Runtime Versions rev every year, the current version in late 2024 is v8 with v9 getting released in November.&lt;/p&gt;
&lt;p&gt;The good news is that most components today still use multi-targeting and support both .NET Framework (or .NET Standard which is .NET Framework compatible) and .NET Core targeting and you can generally find .NET Framework components that work more easily in FoxPro.&lt;/p&gt;
&lt;blockquote&gt;
&lt;h4 id="--stick-to-net-framework"&gt;&lt;i class="fas fa-lightbulb" style="font-size: 1.1em"&gt;&lt;/i&gt;  Stick to .NET Framework&lt;/h4&gt;
&lt;p&gt;Bottom Line: If at all possible aim for using .NET Framework if you're calling .NET code from FoxPro. Only rely on .NET Core components if there is no alternative in .NET Framework available.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="why-net-for-foxpro"&gt;Why .NET for FoxPro?&lt;/h2&gt;
&lt;p&gt;.NET is a Microsoft platform and for better or for worse, it's the only platform that has good Windows support for desktop applications and that can be integrated relatively easily with Visual FoxPro, mainly because it works with COM.&lt;/p&gt;
&lt;p&gt;FoxPro is very old technology, with the last released version now more than 15 years out of date, so in order to keep FoxPro applications up to date with current functionality &lt;strong&gt;some technology&lt;/strong&gt; is needed to extend FoxPro beyond it's old feature set. The original extension technology was COM, but COM has died a slow death right alongside FoxPro, especially for high level languages that use &lt;code&gt;IDispatch&lt;/code&gt; interface as FoxPro does. You'll be hard pressed to find a 'new' COM component that exposes business or system functionality these days. While COM is still heavily used internally in Windows with C++ code, most of the system functionality exposed to developers these days is exposed via .NET.&lt;/p&gt;
&lt;p&gt;Further, these days it's fairly easy to get started building your own .NET components, which facilitates the ability to access more complex .NET code &lt;strong&gt;using your own .NET code&lt;/strong&gt; that acts as a front end for the more complex .NET code.&lt;/p&gt;
&lt;p&gt;This opens up many avenues of extensibility, both in terms of .NET component feature availability to your application, but also as an avenue for extensibility of your application using an alternate technology. What's nice about using a component based interface like this is that you ease into building functionality selectively in .NET and expose it from FoxPro. It's a great way to experiment with .NET for real world scenarios starting with small components, to potentially larger integrations down the road or even entire switching to a .NET based application eventually with the ability to use the code you've built along the way.&lt;/p&gt;
&lt;p&gt;Because .NET is now multi-platform and can run desktop, Web, API, Phone and IOT based applications, any components you build also can be used in all of those environments (assuming they are platform agnostic), so any investments you make in .NET components can be re-used in other .NET based interfaces or even other .NET integrations. So today you might be building an extension interface for your FoxPro desktop application, but tomorrow you may be using that same extension in a .NET Web API application.&lt;/p&gt;
&lt;p&gt;In short, for FoxPro developers .NET offers a path to extensibility via COM integration, that lets you take advantage of most of .NET's features with minimal effort.&lt;/p&gt;
&lt;p&gt;Let's take a look at what that looks like.&lt;/p&gt;
&lt;h2 id="what-is-wwdotnetbridge"&gt;What is wwDotnetBridge?&lt;/h2&gt;
&lt;p&gt;wwDotnetBridge is an &lt;strong&gt;open source, MIT licensed&lt;/strong&gt; FoxPro library, that allows you to load and call most .NET components from FoxPro. It provides registrationless activation of .NET Components and helpers that facilitate accessing features that native COM Interop does not support.&lt;/p&gt;
&lt;p&gt;.NET has built-in COM Interop support which allows you to instantiate and then invoke .NET objects via COM. Unfortunately this mechanism is very limited due to the requirement that components are registered in a special way to both COM and .NET, and that components have to be explicitly marked. Effectively this means you can only instantiate COM components that you have created your self.&lt;/p&gt;
&lt;p&gt;To work around this wwDotnetBridge provides many improvements that work around the limitations of native .NET COM Interop, while still using COM Interop for the inter-process communication. Everything that works with native COM Interop also works with wwDotnetBridge - it's the same technology after all -  but you get many more support features to work around the limitations.&lt;/p&gt;
&lt;p&gt;The key features of wwDotnetBridge are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Registrationless access to most .NET Components&lt;/strong&gt;&lt;br&gt;
Unlike native COM Interop, you can instantiate and access .NET Components and static classes, without requiring those classes to be registered as COM objects. Objects are instantiated from within .NET, so you can access most .NET components by directly loading them from their DLL assembly. Both .NET Framework (&lt;code&gt;wwDotnetBridge&lt;/code&gt;) and .NET Core (&lt;code&gt;wwDotnetCoreBridge&lt;/code&gt;) are supported.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Instantiates and Interacts with .NET Objects via COM from within .NET&lt;/strong&gt;&lt;br&gt;
wwDotnetBridge is a .NET based component that &lt;strong&gt;runs inside of .NET&lt;/strong&gt; and acts as an intermediary for activation, invocation and access operations. A key feature is that it creates .NET instances from within .NET and returns those references using COM Interop. Once loaded you can use all features that COM supports directly: Property access and method calls etc. &lt;em&gt;as long the members accessed use types that are supported by COM&lt;/em&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Support for Advanced .NET Features that COM Interop doesn't support&lt;/strong&gt;&lt;br&gt;
Unfortunately there are many .NET features that COM and FoxPro don't natively support directly: Anything related to .NET Generics, overloaded methods, value types, enums, various number types to name just a few. But because wwDotnetBridge runs inside of .NET, it provides automatic conversions and helpers to allow access to these features via intermediary Reflection operations. These helpers access the unsupported COM operations from inside of .NET and translate the results into COM and FoxPro compatible results that are returned into your FoxPro application.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Automatic Type Conversions&lt;/strong&gt;&lt;br&gt;
Because there are many incompatible types in .NET that don't have equivalents in COM or FoxPro, wwDotnetBridge performs many automatic type conversions. These make it easier to call methods or retrieve values from .NET by automatically converting compatible types. For example: decimals to double, long, byte to int, Guid to string etc. There are also wrapper classes like &lt;code&gt;ComArray&lt;/code&gt; that wraps  .NET Arrays and Collections and provides a FoxPro friendly interface for navigating and updating collections, and &lt;code&gt;ComValue&lt;/code&gt; which wraps incompatible .NET values and provides convenient methods to set and retrieve the value in a FoxPro friendly way and pass it to .NET methods or property assignments.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Support for Async Code Execution&lt;/strong&gt;&lt;br&gt;
A lot of modern .NET Code uses async functionality via &lt;code&gt;Task&lt;/code&gt; based interfaces, and wwDotnetBridge includes a &lt;code&gt;InvokeTaskMethodAsyc()&lt;/code&gt; helper that lets you call these async methods and receive results via Callbacks asynchronously. You can also run &lt;strong&gt;any&lt;/strong&gt; .NET synchronous method and call it asynchronously using &lt;code&gt;InvokeMethodAsync()&lt;/code&gt; using the same Callback mechanism. Finally there's also support for handling .NET events with similar callback mechanics.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There's much more, but these are the most common features used in wwDotnetBridge.&lt;/p&gt;
&lt;p&gt;##AD##&lt;/p&gt;
&lt;h2 id="a-quick-primer"&gt;A quick Primer&lt;/h2&gt;
&lt;p&gt;Before we jump in with more explanations lets walk through a simple example that shows how to use wwDotnetBridge in a simple yet common scenario.&lt;/p&gt;
&lt;p&gt;I'm going to use a pre-built sample component from a library that's part of the samples called &lt;code&gt;wwDotnetBridgeDemo.dll&lt;/code&gt; which is one of the simplest things and also very common things we can do.&lt;/p&gt;
&lt;h3 id="setting-up-wwdotnetbridge"&gt;Setting up wwDotnetBridge&lt;/h3&gt;
&lt;p&gt;The first step is that you need wwDotnetBridge. wwDotnetBridge comes in two versions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Free Open Source Version&lt;/strong&gt;&lt;br&gt;
This version is available for free with source code from GitHub. You can go to the repo and copy the files out of the &lt;code&gt;/Distribution&lt;/code&gt; folder. Copy these files into your FoxPro application path.&lt;br&gt;
&lt;em&gt;uses CrlLoader.dll as the Win32 connector&lt;/em&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Commercial Version in Web Connection and West Wind Client Tools&lt;/strong&gt;&lt;br&gt;
This version of wwDotnetBridge includes a few additional convenience features and .NET components that are not provided in free version. The core feature set however is identical. Unlike the open source version this version uses &lt;code&gt;wwIPstuff.dll&lt;/code&gt; as the loader.&lt;br&gt;
&lt;em&gt;uses wwIPstuff.dll as the Win32 connector&lt;/em&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The three files you need for wwDotnetBridge are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CrlLoader.dll (OSS) or wwIPstuff.dll (Commercial)&lt;/li&gt;
&lt;li&gt;wwDotnetBridge.dll&lt;/li&gt;
&lt;li&gt;wwDotnetBridge.prg&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Copy these into your root project folder. &lt;code&gt;CrlLoader.dll&lt;/code&gt; or &lt;code&gt;wwIPstuff.dll&lt;/code&gt;  &lt;strong&gt;have to live in the root folder&lt;/strong&gt; the other two can live along your FoxPro path.&lt;/p&gt;
&lt;blockquote&gt;
&lt;h4 id="--loading-dlls-from-network-locations-configuration-required"&gt;&lt;i class="fas fa-info-circle" style="font-size: 1.1em"&gt;&lt;/i&gt;  Loading DLLs from Network Locations: Configuration required&lt;/h4&gt;
&lt;p&gt;.NET components require explicit configuration in order to support &lt;strong&gt;remote loading from network locations&lt;/strong&gt;. This is done by creating a configuration file for your application &lt;code&gt;yourapp.exe.config&lt;/code&gt; or the VFP IDE &lt;code&gt;vfp9.exe.config&lt;/code&gt;, in their respective startup folders. We recommend at minimum you use the following &lt;code&gt;.config&lt;/code&gt; file settings:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-xml"&gt;&amp;lt;?xml version=&amp;quot;1.0&amp;quot;?&amp;gt;
&amp;lt;configuration&amp;gt;
  &amp;lt;runtime&amp;gt;
      &amp;lt;loadFromRemoteSources enabled=&amp;quot;true&amp;quot;/&amp;gt;
  &amp;lt;/runtime&amp;gt;
&amp;lt;/configuration&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;p&gt;For all examples in this article I use the GitHub repo's root folder as my base directory from which to run the samples. There's a &lt;code&gt;.\bin&lt;/code&gt; folder that contains all .NET assemblies and for this sample I'll use the &lt;code&gt;bin\wwDotnetBridgeDemos.dll&lt;/code&gt; assembly.&lt;/p&gt;
&lt;p&gt;I'll start with the entire bit of code and we'll break it down afterwards:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;*** Set Environment - path to .\bin and .\classes folder
DO _STARTUP.prg

*** Load wwDotnetBridge
do wwDotNetBridge                 &amp;amp;&amp;amp; Load library
LOCAL loBridge as wwDotNetBridge  &amp;amp;&amp;amp; for Intellisense
loBridge = GetwwDotnetBridge()    &amp;amp;&amp;amp; instance

*** Load an Assembly
? loBridge.LoadAssembly(&amp;quot;wwDotnetBridgeDemos.dll&amp;quot;)

*** Create an class Instance
loPerson = loBridge.CreateInstance(&amp;quot;wwDotnetBridgeDemos.Person&amp;quot;)

*** Access simple Properties - plain COM
? loPerson.Name
? loPerson.Company
? loPerson.Entered

*** Call a Method
? loPerson.ToString()
? loPerson.AddAddress(&amp;quot;1 Main&amp;quot;,&amp;quot;Fairville&amp;quot;,&amp;quot;CA&amp;quot;,&amp;quot;12345&amp;quot;)

*** Special Properties - returns a ComArray instance
loAddresses = loBridge.GetProperty(&amp;quot;Addresses&amp;quot;)  
? loAddresses.Count     &amp;amp;&amp;amp; Number of items in array
loAddress = loAddresses.Item(0)
? loAddress.Street
? loAddress.ToString()
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="loading-wwdotnetbridge-and-loading-your-first-net-library"&gt;Loading wwDotnetBridge and Loading your first .NET Library&lt;/h3&gt;
&lt;p&gt;The first step is to load the FoxPro &lt;code&gt;wwDotnetBridge&lt;/code&gt; class:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;do wwDotNetBridge                 &amp;amp;&amp;amp; Load library
LOCAL loBridge as wwDotNetBridge  &amp;amp;&amp;amp; for Intellisense
loBridge = GetwwDotnetBridge()    &amp;amp;&amp;amp; instance
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first line loads the library into FoxPro's procedure stack so that they library is available. Typically you'd do this at the top of your application once. The &lt;code&gt;LOCAL&lt;/code&gt; declaration is optional and only done for Intellisense.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;GetwwDotnetBridge()&lt;/code&gt; is a helper function that creates a cached instance of wwDotnetBridge that stays loaded even if &lt;code&gt;loBridge&lt;/code&gt; goes out of scope. This is to minimize the overhead of having to reload or check to reload the .NET Runtime.&lt;/p&gt;
&lt;h3 id="loading-assemblies-and-creating-an-instance"&gt;Loading Assemblies and Creating an Instance&lt;/h3&gt;
&lt;p&gt;Next you want to load an assembly (a DLL) which loads the functionality of that library and makes it accessible so that we can access functionality in it.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;*** Load a .NET Assembly
if !loBridge.LoadAssembly(&amp;quot;wwDotnetBridgeDemos.dll&amp;quot;) 
   ? &amp;quot;ERROR: &amp;quot; + loBridge.cErrorMsg
   RETURN
endif   
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;LoadAssembly is used to load a DLL from the current folder or any folder in your FoxPro path. LoadAssembly returns &lt;code&gt;.T.&lt;/code&gt; or &lt;code&gt;.F.&lt;/code&gt; and you can check the &lt;code&gt;.cErrorMsg&lt;/code&gt; for more information if an error occurs.&lt;/p&gt;
&lt;p&gt;Once the assembly is loaded you can create object within it. In this case I want to create a Person class:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;*** Create an class Instance
loPerson = loBridge.CreateInstance(&amp;quot;wwDotnetBridgeDemos.Person&amp;quot;)
IF VARTYPE(loPerson) # &amp;quot;O&amp;quot;
   ? &amp;quot;ERROR: &amp;quot; + loBridge.cErrorMsg
   RETURN
ENDIF
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;.CreateInstance()&lt;/code&gt; method takes a fully qualified type name which corresponds to the .NET &lt;code&gt;namespace.classname&lt;/code&gt; (case sensitive). A namespace is an identifier that's use to separate types with the same name from each other so that if two vendors have components with the same name, they are still separated by their namespaces.&lt;/p&gt;
&lt;p&gt;Again you can check for errors of the instance creation with a non-object type and you can use &lt;code&gt;.cErrorMsg&lt;/code&gt; to get more information on the error.&lt;/p&gt;
&lt;p&gt;Common errors for both of these methods are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Invalid Names - including case. Make sure you name things EXACTLY&lt;/li&gt;
&lt;li&gt;Missing dependencies (more on that later)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="direct-invocation-of-members"&gt;Direct Invocation of Members&lt;/h3&gt;
&lt;p&gt;If the class was created you have now received back a .NET Component in the form of a COM object and you can access any properties and methods that have &lt;strong&gt;simple types&lt;/strong&gt; directly. Essentially any properties and methods that contain COM compatible types can be accessed directly.&lt;/p&gt;
&lt;p&gt;So all of the following works:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;*** Access simple Properties - plain COM
? loPerson.Name
? loPerson.Company
? loPerson.Entered

loPerson.Name = &amp;quot;Jane Doe&amp;quot;
loPerson.Company = &amp;quot;Team Doenut&amp;quot;
loPerson.Entered = DateTime()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Likewise you can call simple methods:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;loPerson.ToString()
? loPerson.AddAddress(&amp;quot;1 Main&amp;quot;,&amp;quot;Fairville&amp;quot;,&amp;quot;CA&amp;quot;,&amp;quot;12345&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;because all of these operations work with simple types they just work with direct access to the .NET object. You can directly call the methods and access properties without any special fix up or proxying.&lt;/p&gt;
&lt;h3 id="proxy-invocation-for-problem-types-and-helpers"&gt;Proxy Invocation for Problem Types and Helpers&lt;/h3&gt;
&lt;p&gt;While simple types work just fine for direct access, .NET has many more types that are not compatible with COM or FoxPro types and can't be converted. This means direct access is simply not possible.&lt;/p&gt;
&lt;p&gt;wwDotnetBridge provides a host of helper functions that effectively proxy indirect access to .NET functionality by translating data coming from FoxPro into .NET and vice versa. By hosting this proxy code inside of .NET, the proxy can access all features of .NET and - as best as possible - translate between the .NET and FoxPro incompatibilities.&lt;/p&gt;
&lt;p&gt;In the example above, the &lt;code&gt;loPerson.Addresses&lt;/code&gt; property is an array, a .NET collection type. Collection types and especially generic lists and dictionaries (&lt;code&gt;List&amp;lt;T&amp;gt;&lt;/code&gt; or &lt;code&gt;Dictionary&amp;lt;TKey, TValue&amp;gt;&lt;/code&gt; ) are very common in .NET for example. However, FoxPro and COM don't support Generics at all, and even basic arrays and lists are not well supported via COM interop. In addition, Value types, Enums or accessing any static members is not supported, but can be accomplished via wwDotnetBridge helpers.&lt;/p&gt;
&lt;p&gt;So &lt;code&gt;loPerson.Addresses&lt;/code&gt; is an array of address objects, so rather than directly accessing it via:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-cs"&gt;loAddress = loPerson.Addresses[1]; &amp;amp;&amp;amp; doesn't work
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;you have to indirectly access the address array which returns a ComArray helper:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-cs"&gt;loAddresses = loBridge.GetProperty(loPerson,&amp;quot;Addresses&amp;quot;);
lnCount = loAddresses.Count
loAddress = loAddress.Item(0);
? loAddress.Street
? loAddress.ToString()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;GetProperty()&lt;/code&gt; is one of the most common helper methods along with &lt;code&gt;InvokeMethod()&lt;/code&gt; and &lt;code&gt;SetProperty()&lt;/code&gt;. Use these methods when direct access does not work or when you know you're dealing with types that don't work via FoxPro or COM.&lt;/p&gt;
&lt;p&gt;These methods use Reflection in .NET to perform their task and you specify an base instance that the operation is applied to (ie. &lt;code&gt;loPerson&lt;/code&gt;) and a Member that is executed as a string (ie. &lt;code&gt;&amp;quot;Addresses&amp;quot;&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;Here's what InvokeMethod looks like:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-cs"&gt;lcName = &amp;quot;Rick&amp;quot;
loPerson.InvokeMethod(loPerson,&amp;quot;DifficultMethod&amp;quot;, lcName) 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There are also methods to invoke static members:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-cs"&gt;? loBridge.InvokeStaticMethod(&amp;quot;System.String&amp;quot;,&amp;quot;Format&amp;quot;, &amp;quot;Hello {0}&amp;quot;, lcName)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There are many more helpers in the class and we'll see more of them in the examples in the later part of this article.&lt;/p&gt;
&lt;p&gt;##AD##&lt;/p&gt;
&lt;h2 id="how-does-wwdotnetbridge-work"&gt;How does wwDotnetBridge Work&lt;/h2&gt;
&lt;p&gt;wwDotnetBridge acts as an intermediary between FoxPro and .NET. In a nutshell, wwDotnetBridge is a loader for the .NET framework, and a proxy interface for FoxPro which allows FoxPro code to pass instructions into .NET code when native direct access to components is not supported. You get the best of both worlds: Native direct COM access when possible, and proxied indirect execution that translates between .NET and COM/FoxPro types to access features that otherwise wouldn't be available.&lt;/p&gt;
&lt;p&gt;Here's a rough outline of how wwDotnetBridge loads and calls a .NET component:&lt;/p&gt;
&lt;h3 id="a-net-loader"&gt;A .NET Loader&lt;/h3&gt;
&lt;p&gt;The most important feature of wwDotnetBridge is that it acts as a loader that makes it possible to access most .NET types - including static members - from FoxPro. There's no registration required as wwDotnetBridge loads components from within .NET and passes them back to FoxPro.&lt;/p&gt;
&lt;p&gt;wwDotnetBridge works like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It loads the .NET Runtime into the FoxPro Process &lt;em&gt;(one time)&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;It bootstraps a .NET &lt;code&gt;wwDotnetBridge.dll&lt;/code&gt; from the loader &lt;em&gt;(one time)&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;wwDotnetBridge&lt;/strong&gt; is the FoxPro ? .NET proxy interface &lt;em&gt;(one time)&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;The loaded proxy instance is passed back to FoxPro &lt;em&gt;(one time)&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;The FoxPro instance is then used to:
&lt;ul&gt;
&lt;li&gt;Load .NET Assemblies (dlls) &lt;em&gt;(once per library)&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;Instantiate .NET Types&lt;/li&gt;
&lt;li&gt;Access 'simple' properties and methods directly via COM&lt;/li&gt;
&lt;li&gt;Invoke problem Methods and access problem Properties via indirect execution&lt;/li&gt;
&lt;li&gt;Convert specialty types that COM doesn't work with directly via specialty helpers&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Figure 1&lt;/strong&gt; show what this looks like in a diagram:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://west-wind.com/wconnect/weblog/imageContent/2025/White-Paper/LoadFlow.gif" alt="Load Flow"&gt;&lt;br&gt;
&lt;small&gt;&lt;strong&gt;Figure 1&lt;/strong&gt; - wwDotnetBridge: Loading a component, creating an instance and accessing members repeatedly&lt;/small&gt;&lt;/p&gt;
&lt;p&gt;Let's break down the parts:&lt;/p&gt;
&lt;p&gt;In order to execute .NET code from FoxPro, the .NET Runtime needs to be loaded into your FoxPro executable or the FoxPro IDE.&lt;/p&gt;
&lt;p&gt;There are a couple of ways this can be done:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Native COM Interop via COM Instantiation&lt;/strong&gt;&lt;br&gt;
.NET has native support for COM instantiation, but it requires that .NET Components are &lt;strong&gt;explicitly&lt;/strong&gt; marked for COM execution which very few are. COM components that you create yourself also have to be marked with explicit COM Interface markers, and have to be registered using a special .NET COM Registration tool. It works, but it's very limited.&lt;br&gt;
&lt;em style="color: red"&gt;not recommended any longer&lt;/em&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;wwDotnetBridge .NET Runtime Hosting&lt;/strong&gt;&lt;br&gt;
wwDotnetBridge includes a small Win32 API loader that bootstraps the .NET Runtime and loads itself into this runtime as the host loader assembly. This is similar to the way native COM Interop hosts the runtime, but it bootstraps the &lt;code&gt;wwDotnetBridge&lt;/code&gt; .NET component that can directly instantiate .NET types &lt;strong&gt;without requiring COM registration&lt;/strong&gt;  or any special marker interfaces/attributes.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="wwdotnetbridge-loader"&gt;wwDotnetBridge Loader&lt;/h3&gt;
&lt;p&gt;The wwDotnetBridge is comprised of three components:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A Win32 native C++ Loader that loads the .NET Runtime&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;wwDotnetBridge&lt;/code&gt; .NET Proxy class that acts as the bridge interface&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;wwDotnetBridge&lt;/code&gt; FoxPro class that calls into the .NET Class to
create classes, invoke methods and set/get properties etc.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The FoxPro class kicks off the process by instantiating the &lt;code&gt;wwDotnetBridge&lt;/code&gt; FoxPro class which internally calls into the Win32 code to check and see whether the .NET Runtime is already running and if not starts it up and bootstraps the wwDotnetBridge .NET class into it. A reference to the .NET Class instance is then marshalled back into FoxPro over COM. The FoxPro class caches this COM references and can now make calls into the .NET Interface.&lt;/p&gt;
&lt;p&gt;Once loaded the &lt;code&gt;wwDotnetBridge&lt;/code&gt; FoxPro class can then communicate with the .NET class by loading assemblies, creating instances and accessing members - in essence it now has full access to the features of the wwDotnetBridge .NET interface.&lt;/p&gt;
&lt;h3 id="direct-member-access-is-via-com"&gt;Direct Member Access is via COM&lt;/h3&gt;
&lt;p&gt;Behind the scenes &lt;strong&gt;.NET Interop relies on COM&lt;/strong&gt; to instantiate types. Both native COM interop and wwDotnetBridge instantiate or access .NET types and those types return their results over COM. Every call that is made to create an instance, access a method or set or get a property happens over COM regardless of whether you use direct access of objects, or you use wwDotnetBridge's helpers.&lt;/p&gt;
&lt;p&gt;Direct COM access is obviously easiest and most efficient from FoxPro and it works with types that are supported by both COM and FoxPro. It works just like you'd expect: You can &lt;strong&gt;call methods and access properties directly on the object instance or use the value as is&lt;/strong&gt;. This works for all 'basic' types, strings, integers, floats, dates, bool values etc. and as long as methods only pass and return these compatible types or properties you can directly access them. Easy!&lt;/p&gt;
&lt;h3 id="indirect-member-access-via-invokemethod-getproperty-and-setproperty"&gt;Indirect Member Access via InvokeMethod(), GetProperty() and SetProperty()&lt;/h3&gt;
&lt;p&gt;But... .NET has a rich type system and certain types and operations don't translate to matching COM or FoxPro types. For example, you can't access static members via COM but you can with wwDotnetBridge. For problem type access wwDotnetBridge automatically provides translations for many types so that they can be used from FoxPro. For example &lt;code&gt;long&lt;/code&gt; and &lt;code&gt;byte&lt;/code&gt; are converted to ints, &lt;code&gt;Guid&lt;/code&gt; is converted to string, &lt;code&gt;DbNull&lt;/code&gt; (a COM null) is converted to &lt;code&gt;null&lt;/code&gt; and so on.&lt;/p&gt;
&lt;p&gt;For these 'problem' scenarios wwDotnetBridge supports &lt;strong&gt;indirect invocation&lt;/strong&gt; which executes operations through the &lt;code&gt;wwDotnetBridge&lt;/code&gt; .NET Component, which proxies the method call or property access from within .NET. Because the code executes within .NET it can use all of .NET's features to access functionality and then translate results in a way that COM and FoxPro supports.&lt;/p&gt;
&lt;p&gt;The most commonly used methods are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;InvokeMethod()&lt;/code&gt; - Invokes a method on an object&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GetProperty()&lt;/code&gt; and &lt;code&gt;SetProperty()&lt;/code&gt; - Gets or sets a property on an object&lt;/li&gt;
&lt;li&gt;&lt;code&gt;InvokeStaticMethod()&lt;/code&gt; - call a static method by name&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SetProperty()&lt;/code&gt; and &lt;code&gt;GetStaticProperty()&lt;/code&gt; - gets or sets a static property by name&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These methods automatically translate inbound and outbound parameters so that they work correctly in .NET and FoxPro.&lt;/p&gt;
&lt;h3 id="wwdotnetbridge-proxy-type-wrappers-comarray-and-comvalue"&gt;wwDotnetBridge Proxy Type Wrappers: ComArray and ComValue&lt;/h3&gt;
&lt;p&gt;wwDotnetBridge provides wrapper types like &lt;a href="https://webconnection.west-wind.com/docs/_3m90jxxxm.htm"&gt;ComArray&lt;/a&gt; and &lt;a href="https://webconnection.west-wind.com/docs/_3481232sd.htm"&gt;ComValue&lt;/a&gt; that automatically wrap types that are not directly supported via COM. &lt;code&gt;ComArray&lt;/code&gt; is automatically returned for .NET Arrays, Lists and Collections and it provides the ability to access list items (via &lt;code&gt;Items(n)&lt;/code&gt;) and the ability manipulate the list by adding, editing and removing items without the collection ever leaving .NET. Many collection types - especially Generic Ones - cause errors if any member is accessed at all, and so leaving the type in .NET makes it possible to manipulate it and still pass it to methods or assign it to a property.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ComValue&lt;/code&gt; is similar, and it provides a mechanisms for storing a .NET value with helper methods that convert to and from FoxPro types to extract and set the .NET stored value.&lt;/p&gt;
&lt;p&gt;Both ComValue and ComArray can be passed to the &lt;strong&gt;indirect&lt;/strong&gt; calling methods, and are automatically translated into their underlying types when passed. So if a method requires a &lt;code&gt;List&amp;lt;T&amp;gt;&lt;/code&gt; parameter, you can pass a &lt;code&gt;ComArray&lt;/code&gt;  and &lt;code&gt;InvokeMethod()&lt;/code&gt; fixes up the &lt;code&gt;ComArray&lt;/code&gt; into the required &lt;code&gt;List&amp;lt;T&amp;gt;&lt;/code&gt; typed parameter.&lt;/p&gt;
&lt;p&gt;These features and helpers make it possible to access most of .NET's functionality from FoxPro.&lt;/p&gt;
&lt;p&gt;##AD##&lt;/p&gt;
&lt;h2 id="working-with-net-from-foxpro-finding-what-you-need"&gt;Working with .NET From FoxPro: Finding what you need!&lt;/h2&gt;
&lt;p&gt;One of the biggest hurdles to using .NET from FoxPro is that &lt;strong&gt;you have to figure out what's available, which components you need and what you can call in a .NET component&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Let's look at these 3 scenarios:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;How do I get the DLLs I need&lt;/li&gt;
&lt;li&gt;What dependencies are there&lt;/li&gt;
&lt;li&gt;What methods and properties are there to access&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="figuring-out-what-assemblies-dlls-to-provide"&gt;Figuring out what Assemblies (DLLs) to Provide&lt;/h3&gt;
&lt;p&gt;First and foremost you need to figure out what components you want to use. There are typically three kinds:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Built-in .NET Framework components (built-in)&lt;/li&gt;
&lt;li&gt;Third Party libraries provided as binary files (dlls)&lt;/li&gt;
&lt;li&gt;Third Party or Microsoft Components that are distributed as NuGet packages (NuGet Packages ? dlls)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="raw-dll-distribution"&gt;Raw DLL Distribution&lt;/h4&gt;
&lt;p&gt;In the old days of .NET there were only DLLs. Everything was either in the GAC, or distributed as a DLL that was attached as a reference to a project. That still works even in modern .NET although raw DLL usage tends to be rare these days as most components are shared as NuGet packages.&lt;/p&gt;
&lt;p&gt;Built-in .NET Components that are part of the core framework are easiest as they are built into the .NET framework or they are located in the Global Assembly Cache. Most of these components are pre-loaded as part of the runtime, so you don't even need to call &lt;code&gt;.LoadAssembly()&lt;/code&gt; to first load an assembly. Those that aren't pre-loaded can just be referenced by their assembly name.&lt;/p&gt;
&lt;p&gt;This is obviously easiest because you don't really have to figure out where the DLLs are coming from. To figure out where runtime files live you can look at the .NET documentation - for a specific class. Each class tells you which namespace and the DLL file name.&lt;/p&gt;
&lt;p&gt;The next option is to provide the Dll files for you to use. This is not very common any more as NuGet has taken over that space, but it's still relevant especially when you build your own .NET Components and distribute them. When you build your own components you generally build them into a DLL and then either build directly into the folder where they are needed or you copy them over to that location. At that point you just use &lt;code&gt;.LoadAssembly()&lt;/code&gt; and you're on your way.&lt;/p&gt;
&lt;h4 id="nuget-package-distribution"&gt;NuGet Package Distribution&lt;/h4&gt;
&lt;p&gt;These days the most common way .NET components are distributed is via &lt;a href="https://www.nuget.org/"&gt;NuGet packages&lt;/a&gt; - and for good reason. By publishing a component as a package an author can make that package available on the NuGet site and the component can be easily shared and accessed by millions of .NET developers as any component shows up in the &lt;a href="http://www.nuget.org"&gt;NuGet package directory&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;NuGet Packages are Zip files with &lt;code&gt;.nupkg&lt;/code&gt; extension, that contain the published package Dlls, potentially for multiple .NET target platforms (ie. net472, net8.0). But the NuGet package Zip file &lt;strong&gt;does not contain any of its dependencies&lt;/strong&gt;. Instead dependencies are resolved and unpacked as part of the .NET Build process using either &lt;code&gt;dotnet build&lt;/code&gt; or &lt;code&gt;dotnet restore&lt;/code&gt; in the SDK.&lt;/p&gt;
&lt;p&gt;FoxPro doesn't have NuGet support, so we have to first manually 'unpack' NuGet packages and their dependencies in a way that we can capture all the required DLLs including the dependencies and add them to our own  projects.&lt;/p&gt;
&lt;blockquote&gt;
&lt;h4 id="--make-sure-dependencies-are-provided"&gt;&lt;i class="fas fa-info-circle" style="font-size: 1.1em"&gt;&lt;/i&gt;  Make sure Dependencies are provided&lt;/h4&gt;
&lt;p&gt;It's crucially important that when you reference an assembly (dll) that all of its dependencies - and the right versions thereof - are also available in the same location or the app's root folder. The .NET loader has to be able to find the dependencies.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;To give you an idea of a dependency graph, here's an example we'll use later on &lt;a href="https://github.com/RickStrahl/Westwind.Ai"&gt;Westwind.Ai&lt;/a&gt; which talks to OpenAI services:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://west-wind.com/wconnect/weblog/imageContent/2025/White-Paper/NugetDependencies.png" alt="Nuget Dependencies"&gt;&lt;br&gt;
&lt;small&gt;&lt;strong&gt;Figure 2&lt;/strong&gt; - NuGet packages can have dependencies that need to be available in order for a component to work. &lt;/small&gt;&lt;/p&gt;
&lt;p&gt;Notice that this &lt;strong&gt;Westwind.AI&lt;/strong&gt; package has a dependency on &lt;a href="https://github.com/RickStrahl/Westwind.Utilities"&gt;Westwind.Utilities&lt;/a&gt; which in turn has a dependency on &lt;a href="https://www.newtonsoft.com/json"&gt;Newtonsoft.Json&lt;/a&gt;. All three of these are required to load the component - miss one and the component won't load or break later at runtime when the relevant library is used. Make sure you have all of the dependencies available.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Note that many .NET packages don't have external dependencies, and in that case you can actually unzip and grab the DLLs directly. Just be sure that no other dependencies are required as it's often hard to debug assembly load errors after the fact only to find out you're missing a nested assembly.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="finding-dlls-unpacking-nuget-packages-and-their-dependencies-using-a-net-project"&gt;Finding DLLs: Unpacking NuGet Packages and their Dependencies using a .NET Project&lt;/h3&gt;
&lt;p&gt;If you have a NuGet package and you want to 'unpack' it and all of its dependencies there's a 'proper' way of doing it to ensure you capture all dependencies. To resolve all of these dependencies into DLLs we can actually use with wwDotnetBridge in FoxPro, I recommend that you &lt;a href="https://dotnet.microsoft.com/en-us/download"&gt;install the .NET SDK&lt;/a&gt; and build a tiny project that loads the NuGet package. That creates build output, along with all the dependent DLLs that you can then copy into your FoxPro project folder.&lt;/p&gt;
&lt;p&gt;The SDK is a one time, low impact install and it's required in order to build .NET code.&lt;/p&gt;
&lt;p&gt;To do this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Create a new Console project by copying &lt;code&gt;.csproj&lt;/code&gt; and &lt;code&gt;program.cs&lt;/code&gt; files&lt;/li&gt;
&lt;li&gt;Add a package reference to the desired component&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;dotnet build&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Go to the &lt;code&gt;/bin/Release/net472&lt;/code&gt; folder&lt;/li&gt;
&lt;li&gt;Pick up the DLLs and copy to your application&lt;/li&gt;
&lt;li&gt;Or: reference DLLs in that folder&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I use a Console project because it's most lightweight top level executable project you can build.&lt;/p&gt;
&lt;p&gt;The project file can be totally generic so lets do it just by creating a couple of files:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Create a folder name it &lt;code&gt;ConsoleTest&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Create a file called &lt;code&gt;ConsoleTest.csproj&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Create a file called &lt;code&gt;program.cs&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;dotnet build&lt;/code&gt; from that folder&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here's the content of these files. The &lt;code&gt;.csproj&lt;/code&gt; project file looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-xml"&gt;&amp;lt;Project Sdk=&amp;quot;Microsoft.NET.Sdk&amp;quot;&amp;gt;
  &amp;lt;PropertyGroup&amp;gt;
    &amp;lt;OutputType&amp;gt;Exe&amp;lt;/OutputType&amp;gt;
    &amp;lt;TargetFramework&amp;gt;net472&amp;lt;/TargetFramework&amp;gt;
  &amp;lt;/PropertyGroup&amp;gt;

  &amp;lt;ItemGroup&amp;gt;
    &amp;lt;PackageReference Include=&amp;quot;Westwind.AI&amp;quot; Version=&amp;quot;*&amp;quot; /&amp;gt;    
    &amp;lt;!--&amp;lt;PackageReference Include=&amp;quot;GoogleAuthenticator&amp;quot; Version=&amp;quot;*&amp;quot; /&amp;gt;--&amp;gt;
    &amp;lt;!--&amp;lt;PackageReference Include=&amp;quot;WeCantSpell.Hunspell&amp;quot; Version=&amp;quot;*&amp;quot; /&amp;gt;--&amp;gt;
  &amp;lt;/ItemGroup&amp;gt;
&amp;lt;/Project&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Put the file into its own folder like &lt;code&gt;ConsoleTest&lt;/code&gt; and name the project &lt;code&gt;ConsoleTest.csproj&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Also create a file called &lt;code&gt;program.cs&lt;/code&gt; file as the main program:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-cs"&gt;using System;

namespace ConsoleStart {
    public class Program 
    {
        public static void Main(string[] args) {
            Console.WriteLine(&amp;quot;Hello World!&amp;quot;);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;h4 id="--dotnet-new-doesnt-work-for-net-framework-projects"&gt;&lt;i class="fas fa-info-circle" style="font-size: 1.1em"&gt;&lt;/i&gt;  &lt;code&gt;dotnet new&lt;/code&gt; doesn't work for .NET Framework Projects&lt;/h4&gt;
&lt;p&gt;The SDK  has built-in tools to create projects using &lt;code&gt;dotnet new console -n ConsoleTest&lt;/code&gt;, but unfortunately newer SDK versions no longer support .NET Framework, even though it does support compilation of .NET Framework code. The problem is that new project types include a number of C# language features that are not supported in .NET Framework, so rather than removing and changing settings, it's simply easier to create or copy the files manually as shown above.&lt;/p&gt;
&lt;p&gt;You can find the above template in the &lt;code&gt;/dotnet/New Project net472 Template&lt;/code&gt;. To create a new project, copy the folder and rename the folder and project name and you're ready to go.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;With these files in place and the .NET SDK installed, open a Terminal window in the &lt;code&gt;ConsoleTest&lt;/code&gt; folder and run:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-ps"&gt;dotnet build
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output gets generated into the &lt;code&gt;./bin/Debug/net472&lt;/code&gt; folder. Here's what that looks like using Visual Studio Code as the editor with its built-in Terminal &lt;em&gt;(note: you can use whatever editor you like)&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://west-wind.com/wconnect/weblog/imageContent/2025/White-Paper/DotnetBuildForDependencies.png" alt="Dotnet Build For Dependencies"&gt;&lt;br&gt;
&lt;small&gt;&lt;strong&gt;Figure 3&lt;/strong&gt; - Building a tiny .NET project with the NuGet package you want to use, produces all DLLs and dependencies you can copy into your own project.&lt;/small&gt;&lt;/p&gt;
&lt;p&gt;As you can see the output compilation folder contains all 3 of the assemblies from the 3 NuGet packages and you can now copy those DLLs into your application folder.&lt;/p&gt;
&lt;p&gt;You can create this project once, and simply add different package references into the project, delete the &lt;code&gt;bin&lt;/code&gt; folder, then build again to figure out what dependencies you need to deploy:&lt;/p&gt;
&lt;h3 id="use-a-disassembler-tool-to-discover-net-types-and-members"&gt;Use a Disassembler Tool to discover .NET Types and Members&lt;/h3&gt;
&lt;p&gt;Once you have the DLLs and you can use them successfully with &lt;code&gt;LoadAssembly()&lt;/code&gt; the next step is to find the types (classes) you want to load and the members you want to call on them.&lt;/p&gt;
&lt;p&gt;There are many tools available that provide IL code inspection that can show you class hierarchies in a DLL:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/icsharpcode/ILSpy"&gt;ILSpy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.jetbrains.com/decompiler/"&gt;JetBrain DotPeek&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Reflector (old, free version included in &lt;code&gt;.\tools&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each tool is a little different, but most modern tools use live decompilation to show the class tree and member signatures. &lt;strong&gt;Figure 2&lt;/strong&gt; shows the Markdig Markdown library in ILSpy:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://west-wind.com/wconnect/weblog/imageContent/2025/White-Paper/InspectingClassSignatures.png" alt="Inspecting Class Signatures"&gt;&lt;br&gt;
&lt;small&gt;&lt;strong&gt;Figure 4&lt;/strong&gt; - Inspecting .NET Libraries for call signatures and class naming in ILSpy&lt;/small&gt;&lt;/p&gt;
&lt;p&gt;There are a number of things to be aware of:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Classes are instantiated by using &lt;code&gt;namespace.classname&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;All type names and members &lt;strong&gt;are case sensitive&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;All methods require &lt;strong&gt;exact&lt;/strong&gt; parameter matches (default values are not supported over COM)&lt;/li&gt;
&lt;li&gt;Overloaded methods cannot be called directly&lt;/li&gt;
&lt;li&gt;If there are overloads make sure you use the &lt;strong&gt;exact&lt;/strong&gt; parameter types&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The above &lt;code&gt;ToHtml()&lt;/code&gt; method - which happens to be a static method that doesn't require an objec instance - would be called like this from FoxPro:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;loBridge = GetwwDotnetBridge()
loBridge.LoadAssembly(&amp;quot;Markdig.dll&amp;quot;)
lcHtml = loBridge.InvokeStaticMethod(&amp;quot;Markdig.Markdown&amp;quot;,&amp;quot;ToHtml&amp;quot;,lcMarkdown,null)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="use-linqpad-to-test-out-code"&gt;Use LinqPad to Test Out Code&lt;/h3&gt;
&lt;p&gt;Another extremely useful tool is &lt;a href="https://www.linqpad.net/"&gt;LinqPad&lt;/a&gt; which you can think of as a Command Window for .NET. Like the FoxPro Command Window you can use LinqPad to test out code interactively. If you didn't create the component you're trying to access from .NET it's a good idea to try it out before you actually try to call it from FoxPro.&lt;/p&gt;
&lt;p&gt;This is useful for several reasons:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Makes sure you are calling the code correctly, and it works in .NET&lt;br&gt;
&lt;small&gt;&lt;em&gt;if it doesn't work in .NET, it sure as hell won't work in FoxPro!&lt;/em&gt;&lt;/small&gt;&lt;/li&gt;
&lt;li&gt;Lets you check for type information interactively&lt;br&gt;
&lt;small&gt;&lt;em&gt;hover over type declaration to see the &lt;code&gt;namespace.classname&lt;/code&gt; and &lt;code&gt;class&lt;/code&gt;&lt;/em&gt;&lt;/small&gt;&lt;/li&gt;
&lt;li&gt;Lets you see overloads and test with different values/types&lt;br&gt;
&lt;small&gt;&lt;em&gt;hover over a method and check for a drop down list (n overloads)&lt;/em&gt;&lt;/small&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There are two versions of LinqPad available for different scenarios:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;LinqPad 5.0  works with .NET Framework&lt;/li&gt;
&lt;li&gt;LinqPad Latest (8.0 currently) works with .NET Core&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Although in many cases both .NET Framework and Core work the same there are differences, so if you're testing for FoxPro you generally prefer using LinqPad 5.0 to test the .NET Framework versions of components.&lt;/p&gt;
&lt;p&gt;Here's LinqPad checking out the Spell Checking example code:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://west-wind.com/wconnect/weblog/imageContent/2025/White-Paper/LinqPad.png" alt="LinqPad"&gt;&lt;br&gt;
&lt;small&gt;&lt;strong&gt;Figure 5&lt;/strong&gt; -  Using LinqPad to test out .NET functionality before creating FoxPro code.&lt;/small&gt;&lt;/p&gt;
&lt;p&gt;LinqPad is an awesome tool if you're using .NET in general - it allows you to create small snippets for testing as I've shown, but you can also use it as a tool to create small utilities like converters, translators and general purpose tools that you can readily save and then later load. For example, I have several converters that convert TypeScript classes to C# and vice versa, de-dupe lists of email addresses and many other things that are basically stored as LinqPad scripts that I can pull up and tweak or paste different source text into.&lt;/p&gt;
&lt;h2 id="usage-examples"&gt;Usage Examples&lt;/h2&gt;
&lt;p&gt;Ok enough theory - let's jump in and put all of this into practice with some useful examples that you can use in your own applications.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;wwDotnetBridge 101 â€“ Load, Create, Invoke, Get/Set&lt;/li&gt;
&lt;li&gt;Create a powerful String Formatter&lt;/li&gt;
&lt;li&gt;Add Markdown Parsing to your Applications&lt;/li&gt;
&lt;li&gt;Use a Two-Factor Authenticator Library&lt;/li&gt;
&lt;li&gt;Add Spellchecking to your applications&lt;/li&gt;
&lt;li&gt;Humanize numbers, dates, measurements&lt;/li&gt;
&lt;li&gt;File Watcher and Live Reload (Event Handling)&lt;/li&gt;
&lt;li&gt;Async: Use OpenAI for common AI Operations&lt;/li&gt;
&lt;li&gt;Async: Print Html to Pdf&lt;/li&gt;
&lt;li&gt;Create a .NET Component and call it from FoxPro&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="wwdotnetbridge-101--load-create-invoke-getset"&gt;wwDotnetBridge 101 â€“ Load, Create, Invoke, Get/Set&lt;/h3&gt;
&lt;p&gt;&lt;i&gt;&lt;small&gt;&lt;/small&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Demonstrates:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Basics of wwDotnetBridge&lt;/li&gt;
&lt;li&gt;Loading the library&lt;/li&gt;
&lt;li&gt;Loading assemblies&lt;/li&gt;
&lt;li&gt;Creating instances&lt;/li&gt;
&lt;li&gt;Accessing members&lt;/li&gt;
&lt;li&gt;Using special unsupported types&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;Lets start with a basic usage example that demonstrates how wwDotnetBridge works.&lt;/p&gt;
&lt;p&gt;For this 101 level example I'm going to use a custom class in custom compiled project I created for examples for this session. We'll talk about how to create this class later, but for now just know that this project creates an external .NET assembly (.dll) from which we'll load a .NET class, and call some of its members.&lt;/p&gt;
&lt;p&gt;Specifically we'll look at how to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Load wwDotnetBridge&lt;/li&gt;
&lt;li&gt;Load an Assembly&lt;/li&gt;
&lt;li&gt;Create a .NET Object instance&lt;/li&gt;
&lt;li&gt;Make native COM calls on the instance&lt;/li&gt;
&lt;li&gt;Invoke or access problem members on an instance&lt;/li&gt;
&lt;li&gt;Use Helper Classes to work with problematic .NET Types&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="simple-invocation"&gt;Simple Invocation&lt;/h4&gt;
&lt;p&gt;The easiest way to look at this is to look at commented example.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;do wwDotNetBridge                 &amp;amp;&amp;amp; Load library
LOCAL loBridge as wwDotNetBridge  &amp;amp;&amp;amp; for Intellisense only
loBridge = GetwwDotnetBridge()    &amp;amp;&amp;amp; Create Cached Instance of wwDotnetBridge

*** Load an .NET Assembly (dll)
loBridge.LoadAssembly(&amp;quot;wwDotnetBridgeDemos.dll&amp;quot;)

*** Create a class Instance - `Namespace.Classname`
loPerson = loBridge.CreateInstance(&amp;quot;wwDotnetBridgeDemos.Person&amp;quot;)

*** Access simple Properties - direct access
? &amp;quot;*** Simple Properties:&amp;quot; 
? loPerson.Name
? loPerson.Company
? loPerson.Entered
?

*** Call a Method - direct access
? &amp;quot;*** Method call: Formatted Person Record (ToString):&amp;quot;
? loPerson.ToString()  &amp;amp;&amp;amp; Formatted Person with Address
?

*** Add a new address - direct access
loAddress =  loPerson.AddAddress(&amp;quot;1 Main&amp;quot;,&amp;quot;Fairville&amp;quot;,&amp;quot;CA&amp;quot;,&amp;quot;12345&amp;quot;)

*** Special Properties - returns a ComArray instance
loAddresses = loBridge.GetProperty(loPerson, &amp;quot;Addresses&amp;quot;)  

? loBridge.ToJson(loAddresses, .T.)  &amp;amp;&amp;amp; Commercial only
? TRANSFORM(loAddresses.Count) + &amp;quot; Addresses&amp;quot;     &amp;amp;&amp;amp; Number of items in array

? &amp;quot;*** First Address&amp;quot;
loAddress = loAddresses.Item(0)
? &amp;quot;Street: &amp;quot; + loAddress.Street
? &amp;quot;Full Address (ToString): &amp;quot; + CHR(13) + CHR(10) + loAddress.ToString()
? 

? &amp;quot;*** All Addresses&amp;quot;
FOR lnX = 0 TO loAddresses.Count-1
	loAddress = loAddresses.Item(lnX)
	? loAddress.ToString()
	?
ENDFOR
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first steps are pretty straight forward: You create an instance of the wwDotnetBridge object, which you then use to create an instance of a .NET class - or you can also call static methods directly (using &lt;code&gt;.InvokeStaticMethod()&lt;/code&gt; more on that in the next sample).&lt;/p&gt;
&lt;p&gt;Once you have the class you can call its methods and access its properties. For any properties and method signatures that are COM compliant, you can just directly access them the same way as you would for FoxPro members.&lt;/p&gt;
&lt;h4 id="indirect-execution"&gt;Indirect Execution&lt;/h4&gt;
&lt;p&gt;For problem types or some complex types likes arrays and collections, you have to use wwDotnetBridge's indirect invocation methods to access members. The three most common methods are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;InvokeMethod()&lt;/li&gt;
&lt;li&gt;GetProperty()&lt;/li&gt;
&lt;li&gt;SetProperty()&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In this example, the &lt;code&gt;loPerson&lt;/code&gt; instance includes an &lt;code&gt;Addresses&lt;/code&gt; property which contains an array of Address object. While you can retrieve the &lt;code&gt;Addresses&lt;/code&gt; object directly, you can't do anything useful with the array in FoxPro.&lt;/p&gt;
&lt;p&gt;So rather than returning the array &lt;code&gt;.GetProperty()&lt;/code&gt; returns you a ComArray instance instead which lets you access and manipulate the collection:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;*** Returns a ComArray instance
loAddresses = loBridge.GetProperty(loPerson,&amp;quot;Addresses&amp;quot;)

? loAddresses.Count   &amp;amp;&amp;amp; 2
loAddress1 = loAddresses.Item(0)

FOR lnX = 0 to loAddresses.Count -1 
    loAddress = loAddresses.Item(lnX)
    * ? loAddress.Street
    ? loAddress.ToString()
ENDFOR

loNewAddress = loBridge.CreateInstance(&amp;quot;wwDotnetBridge.Address&amp;quot;)
loNewAddress.Street = &amp;quot;122 Newfound Landing&amp;quot;
loAddressses.Add(loNewAddress)

loAddresses.Count   &amp;amp;&amp;amp; 3
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id="using-comarray-for-net-arrays-lists-and-collections"&gt;Using ComArray for .NET Arrays, Lists and Collections&lt;/h4&gt;
&lt;p&gt;Because arrays and collections are ultra-common in .NET here's how you can add a new item to the collection using the same ComArray structure:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;? &amp;quot;*** Add another item to the array&amp;quot;
* loNewAddress = loBridge.CreateInstance(&amp;quot;wwDotnetBridgeDemos.Address&amp;quot;)
loNewAddress = loAddresses.CreateItem()
loNewAddress.Street = &amp;quot;122 Newfound Landing&amp;quot;
loNewAddress.City = &amp;quot;NewFoundLanding&amp;quot;
loAddresses.Add(loNewAddress)

? TRANSFORM(loAddresses.Count) + &amp;quot; Addresses&amp;quot;  &amp;amp;&amp;amp; 3
FOR lnX = 0 to loAddresses.Count -1 
    loAddress = loAddresses.Item(lnX)
    ? loAddress.ToString()
    ? 
ENDFOR
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id="summary"&gt;Summary&lt;/h4&gt;
&lt;p&gt;You've just seen how to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Load a .NET Assembly&lt;/li&gt;
&lt;li&gt;Create a .NET Class from within it&lt;/li&gt;
&lt;li&gt;Call methods and set properties&lt;/li&gt;
&lt;li&gt;Access a complex property and use a helper object&lt;/li&gt;
&lt;li&gt;Work .NET Collections from FoxPro&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="create-a-powerful-string-formatter"&gt;Create a powerful String Formatter&lt;/h3&gt;
&lt;p&gt;&lt;small&gt;&lt;i&gt;&lt;/i&gt;&lt;/small&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Demonstrates:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Using .NET native String functions to format strings&lt;/li&gt;
&lt;li&gt;Calling native .NET methods on objects without assembly loading&lt;/li&gt;
&lt;li&gt;Invoking static methods&lt;/li&gt;
&lt;li&gt;Creating simple wrapper functions for .NET functionality&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;This example is a little more practical: It makes most of .NET's string formatting features available to Visual FoxPro and exposes these as easy to use FoxPro functions. .NET has built-in string formatteing support that allow powerful formatting of things like dates and numbers along with C style string format templates.&lt;/p&gt;
&lt;p&gt;In this example we'll access two native .NET features:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ToString()&lt;/code&gt;&lt;br&gt;
This method is a base method on the lowest level .NET object which is &lt;code&gt;System.Object&lt;/code&gt; and &lt;code&gt;ToString()&lt;/code&gt; exists on every object and value in .NET except &lt;code&gt;null&lt;/code&gt;. Each type can implement a custom implementation relevant to the type, or&lt;/li&gt;
&lt;li&gt;&lt;code&gt;System.String.FormatString()&lt;/code&gt;&lt;br&gt;
Is a C-Style template string expansion method, that can be used to embed values more easily into strings using &lt;code&gt;{n}&lt;/code&gt; value expansion. Additionally &lt;code&gt;FormatString()&lt;/code&gt; supports the same format specifiers that &lt;code&gt;ToString()&lt;/code&gt; supports on any templated values.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="formatting-dates-and-numbers-or-any-formattable-net-type-with-tostring"&gt;Formatting Dates and Numbers or any Formattable .NET Type with ToString()&lt;/h4&gt;
&lt;p&gt;The .NET &lt;code&gt;System.Object&lt;/code&gt; base class exposes a &lt;code&gt;ToString()&lt;/code&gt; method which ensures that &lt;strong&gt;every .NET object and value&lt;/strong&gt; (except &lt;code&gt;null&lt;/code&gt;) has a &lt;code&gt;ToString()&lt;/code&gt; method, which allows you to write out any object as a string. Most common .NET types have practical &lt;code&gt;ToString()&lt;/code&gt; implementations so that a number will write out the number as string, and date writes out in a common date format. More complex objects have custom &lt;code&gt;ToString()&lt;/code&gt; implementations, and if you create your own classes you can override &lt;code&gt;ToString()&lt;/code&gt; with your own string representation that makes sense.&lt;/p&gt;
&lt;p&gt;Additionally &lt;code&gt;ToString()&lt;/code&gt; supports an optional format specifier for many common types, which is specifically useful for numbers and dates since these can be represented in so many different ways.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ToString()&lt;/code&gt; with a format specifier is similar in behavior to &lt;code&gt;Transform()&lt;/code&gt; in FoxPro, except that .NET formatters tend to be much more flexible with many more options.&lt;/p&gt;
&lt;p&gt;The most common formatters are Date and Number formatters, but many other types also have formatters. To do this I'll implement  a &lt;code&gt;FormatValue()&lt;/code&gt; function in FoxPro shown after the examples.&lt;/p&gt;
&lt;p&gt;Let's look at some Date formatting first:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/dotnet/standard/base-types/custom-date-and-time-format-strings"&gt;.NET Date Format Strings Docs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;do _startup.prg

do wwDotNetBridge
LOCAL loBridge as wwDotNetBridge
loBridge = GetwwDotnetBridge()

*** No Format String - Default ToString() behavior
? &amp;quot;Plain FormatValue on Date: &amp;quot;  + FormatValue(DATETIME())
* 6/6/2016 7:49:26 PM

lcFormat = &amp;quot;MMM d, yyyy&amp;quot;
? lcFormat + &amp;quot;: &amp;quot; +  FormatValue(DATETIME(),lcFormat)
* Jun 10, 2016

lcFormat = &amp;quot;MMMM d, yyyy&amp;quot;
? lcFormat + &amp;quot;: &amp;quot; + FormatValue(DATETIME(),lcFormat)
* August 1, 2016

lcFormat = &amp;quot;HH:mm:ss&amp;quot;
? lcFormat + &amp;quot;: &amp;quot; + FormatValue(DATETIME(),lcFormat)
* 20:15:10

cFormat = &amp;quot;h:m:s tt&amp;quot;
? lcFormat + &amp;quot;: &amp;quot; +  FormatValue(DATETIME(),lcFormat)
* 8:5:10 PM

lcFormat = &amp;quot;MMM d @ HH:mm&amp;quot;
? lcFormat + &amp;quot;: &amp;quot; +  FormatValue(DATETIME(),lcFormat)
* Aug 1 @ 20:44

lcFormat = &amp;quot;r&amp;quot;  &amp;amp;&amp;amp; Mime Date Time
? lcFormat + &amp;quot;: &amp;quot; +  FormatValue(DATETIME(),lcFormat)
* Mon, 06 Jun 2016 22:41:33 GMT

lcFormat = &amp;quot;u&amp;quot;  
? lcFormat + &amp;quot;: &amp;quot; +  FormatValue(DATETIME(),lcFormat)
* 2016-06-06 22:41:44Z

lcFormat = &amp;quot;ddd, dd MMM yyyy HH:mm:ss zzz&amp;quot;
? &amp;quot;MimeDateTime: &amp;quot; +  STUFF(FormatValue(DATETIME(),lcFormat),30,1,&amp;quot;&amp;quot;)
* 2016-06-06 22:41:44Z
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There are &lt;strong&gt;a lot&lt;/strong&gt; of different time formats available including fully spelled out versions. By default all date formats are in the currently active user locale (ie. &lt;code&gt;en_US&lt;/code&gt; or &lt;code&gt;de_DE&lt;/code&gt;)  and the value will adjust based on which language you are running your application in. It's also possible to pass a specific .NET Culture to format for some other language and formatting, but that's not supported for the helpers discussed here.&lt;/p&gt;
&lt;p&gt;Number formatting is very similar:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/dotnet/standard/base-types/standard-numeric-format-strings"&gt;.NET Number Format Strings Docs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;? &amp;quot;*** Numberformats&amp;quot;

*** Number formats

lcFormat = &amp;quot;00&amp;quot;  &amp;amp;&amp;amp; fills with leading 0's
? lcFormat + &amp;quot;: &amp;quot; + FormatValue(2,&amp;quot;00&amp;quot;)
* 02

? lcFormat + &amp;quot;: &amp;quot; + FormatValue(12,&amp;quot;00&amp;quot;)
* 12

lcFormat = &amp;quot;c&amp;quot;    &amp;amp;&amp;amp; currency (symbol, separator and default 2 decimals
? lcFormat + &amp;quot;: &amp;quot; +  FormatValue(1233.22,lcFormat)
* $1,233.22

lcFormat = &amp;quot;n2&amp;quot;   &amp;amp;&amp;amp; separators and # of decimals
? lcFormat + &amp;quot;: &amp;quot; +  FormatValue(1233.2255,lcFormat)
* $1,233.23

lcFormat = &amp;quot;n0&amp;quot;   &amp;amp;&amp;amp; separators and no decimals
? lcFormat + &amp;quot;: &amp;quot; +  FormatValue(1233.2255,lcFormat)
* $1,233
?
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To implement the above &lt;code&gt;FormatValue()&lt;/code&gt; function, I use a simple FoxPro wrapper function that looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;************************************************************************
*  FormatValue
****************************************
***  Function: Formats a value using .NET ToString() formatting
***            for whatever the text ends up with
***      Pass:  Pass in any .NET value and call it's ToString()
***             method of the underlying type. This 
***             Optional FormatString (&amp;quot;n2&amp;quot;, &amp;quot;MMM dd, yyyy&amp;quot; etc)
***    Return: Formatted string
************************************************************************
FUNCTION FormatValue(lvValue,lcFormatString)
LOCAL loBridge 

IF ISNULL(lvValue)
   RETURN &amp;quot;null&amp;quot;
ENDIF   

loBridge = GetwwDotnetBridge()

IF EMPTY(lcFormatString)	
	RETURN loBridge.InvokeMethod(lvValue,&amp;quot;ToString&amp;quot;)
ENDIF  

RETURN loBridge.InvokeMethod(lvValue,&amp;quot;ToString&amp;quot;,lcFormatString)
ENDFUNC
*   FormatValue
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This function works off the value that we are passing into .NET and relies on the fact that .NET treats any value or object as an object. So a Date or Number, Boolean all are objects and we can call &lt;code&gt;ToString(format)&lt;/code&gt; on those values. So it's literally a single method call.&lt;/p&gt;
&lt;p&gt;If no parameter is passed we just call &lt;code&gt;ToString()&lt;/code&gt; without parameters, otherwise we call &lt;code&gt;ToString(format)&lt;/code&gt;. Note that each overloaded .NET method requires a separate FoxPro call - even if the .NET method has default values for the method. This is because .NET internally looks at full method signatures and default parameter values are not part of the meta data that is used to match the right signature to call so we &lt;strong&gt;always have to call the exact signature that we want to use&lt;/strong&gt; including potentially missing parameters. This can make translating C# code to FoxPro a little more tricky at times and is one of the reasons you should always verify method signatures in a Dissassembler tool like ILSpy or test methods in LinqPad with the full parameter structure.&lt;/p&gt;
&lt;p&gt;We'll see this even more vividly in the &lt;code&gt;FormatString()&lt;/code&gt; function we'll discuss next as it can take a variable number of parameters.&lt;/p&gt;
&lt;h4 id="string-formatting-with-c-style-string-templates"&gt;String Formatting with C Style String Templates&lt;/h4&gt;
&lt;p&gt;If you're old skool like me, you probably remember &lt;code&gt;printf()&lt;/code&gt; from your C Computer Science classes back in the day. Most C style languages have &lt;code&gt;printf()&lt;/code&gt; style string formatting functionality where you can 'inject' embeddable values into the string. This is not unlike FoxPro's &lt;code&gt;TextMerge()&lt;/code&gt; function, but much more efficient and with the added benefit of the same string formatting available for embedded values as discussed for &lt;code&gt;FormatValue()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Here's what this looks like when called from FoxPro:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;? &amp;quot;*** String Formatting&amp;quot;
? FormatString(&amp;quot;Hey {0}, the date and time is: {1:MMM dd, yyyy - h:mm tt}&amp;quot;,&amp;quot;Rick&amp;quot;,DATETIME())
?

? &amp;quot;*** Brackets need to be double escaped&amp;quot;
? FormatString(&amp;quot;This should escape {{braces}} and format the date: {0:MMM dd, yyyy}&amp;quot;,DATE())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can call &lt;code&gt;FormatString()&lt;/code&gt; with a string 'template' that contains &lt;code&gt;{0-n}&lt;/code&gt; expressions inside of it and you then pass parameters to the functions to fill in the &lt;code&gt;{n}&lt;/code&gt; holes with the parameter values. The numbering is 0 based so you start with &lt;code&gt;{0}&lt;/code&gt; for the first parameter.&lt;/p&gt;
&lt;p&gt;Additionally you can also apply format strings as described in &lt;code&gt;FormatValue()&lt;/code&gt; so you can use &lt;code&gt;{0:MMM dd, yyy}&lt;/code&gt; for a  Date expansion for example.&lt;/p&gt;
&lt;p&gt;Note that &lt;code&gt;FormatString()&lt;/code&gt; uses &lt;code&gt;ToString()&lt;/code&gt; to format the value, so this works with any kind of object, although many actual objects don't implement it and instead return just the object name as &lt;code&gt;Namespace.Classname&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;However, if a class implements a custom &lt;code&gt;ToString()&lt;/code&gt;method, it can do &lt;strong&gt;any kind of custom formatting&lt;/strong&gt; - as I did in the &lt;code&gt;wwDotnetBridge101&lt;/code&gt; example and the &lt;code&gt;Person.ToString()&lt;/code&gt; method, which outputs a full name and address block as a string:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;// Person
public string DisplayName =&amp;gt; (Name ?? string.Empty) +  
                             (!string.IsNullOrEmpty(Company) ?  $&amp;quot; ({Company})&amp;quot; : string.Empty);
                               
public override string ToString()
{
    return DisplayName + &amp;quot;\r\n&amp;quot; +                  
           Addresses.FirstOrDefault();  &amp;amp;&amp;amp; .ToString() is implied! 
}

// Address
public override string ToString()
{
    return Street + &amp;quot;\r\n&amp;quot; + City + &amp;quot;\r\n&amp;quot; + State + &amp;quot; &amp;quot; + Zip;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can then use that in FoxPro simply with:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;*** Load loPerson .NET Object
? loBridge.LoadAssembly(&amp;quot;wwDotnetBridgeDemos.dll&amp;quot;)
loPerson = loBridge.CreateInstance(&amp;quot;wwDotnetBridgeDemos.Person&amp;quot;)
loPerson.Name = &amp;quot;Rick Strahl&amp;quot;
loAddresses = loBridge.GetProperty(loPerson,&amp;quot;Addresses&amp;quot;)
loAddress = loAddresses.Item(0)
loAddress.City = &amp;quot;Anywhere USA&amp;quot;

*** Both of these work
? FormatValue(loPerson)
? loPerson.ToString()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The same also works with &lt;code&gt;FormatString()&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;? FormatString(&amp;quot;Person Object:\r\n{0} and the time is: {1:t}&amp;quot;, loPerson, DATETIME())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;FormatString()&lt;/code&gt; is very powerful and quite useful to quickly create string structures.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;FormatString() also supports several C# string escape characters like &lt;code&gt;\r&lt;/code&gt; &lt;code&gt;\n&lt;/code&gt; and &lt;code&gt;\t&lt;/code&gt; although that's not natively supported as .NET treats a foxPro string as is and escapes any special characters. However my implementation adds explicit support for &lt;code&gt;\n\r\t\0&lt;/code&gt; and escape them before passing to .NET (which has its own issue as you can't de-escape those values))&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Here's what the FoxPro &lt;code&gt;FormatString()&lt;/code&gt; function looks like:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;************************************************************************
*  FormatString
****************************************
***  Function: Uses a string template to embed formatted values
***            into a string.
***    Assume:
***      Pass: lcFormat    -  Format string use {0} - {10} for parameters
***            lv1..lv10   -  Up to 10 parameters
***    Return:
************************************************************************
FUNCTION FormatString(lcFormat, lv1,lv2,lv3,lv4,lv5,lv6,lv7,lv8,lv9,lv10)
LOCAL lnParms, loBridge
lnParms = PCOUNT()
loBridge = GetwwDotnetBridge()

lcFormat = EscapeCSharpString(lcFormat)

DO CASE 
	CASE lnParms = 2
		RETURN loBridge.InvokeStaticMethod(&amp;quot;System.String&amp;quot;,&amp;quot;Format&amp;quot;,lcFormat,lv1)
	CASE lnParms = 3
		RETURN loBridge.InvokeStaticMethod(&amp;quot;System.String&amp;quot;,&amp;quot;Format&amp;quot;,lcFormat,lv1,lv2)
	CASE lnParms = 4
		RETURN loBridge.InvokeStaticMethod(&amp;quot;System.String&amp;quot;,&amp;quot;Format&amp;quot;,lcFormat,lv1,lv2,lv3)
	CASE lnParms = 5
		RETURN loBridge.InvokeStaticMethod(&amp;quot;System.String&amp;quot;,&amp;quot;Format&amp;quot;,lcFormat,lv1,lv2,lv3,lv4)
	CASE lnParms = 6
		RETURN loBridge.InvokeStaticMethod(&amp;quot;System.String&amp;quot;,&amp;quot;Format&amp;quot;,lcFormat,lv1,lv2,lv3,lv4,lv5)
	CASE lnParms = 7
		RETURN loBridge.InvokeStaticMethod(&amp;quot;System.String&amp;quot;,&amp;quot;Format&amp;quot;,lcFormat,lv1,lv2,lv3,lv4,lv5,lv6)
	CASE lnParms = 8
		RETURN loBridge.InvokeStaticMethod(&amp;quot;System.String&amp;quot;,&amp;quot;Format&amp;quot;,lcFormat,lv1,lv2,lv3,lv4,lv5,lv6,lv7)
	CASE lnParms = 9
		RETURN loBridge.InvokeStaticMethod(&amp;quot;System.String&amp;quot;,&amp;quot;Format&amp;quot;,lcFormat,lv1,lv2,lv3,lv4,lv5,lv6,lv7,lv8)
	CASE lnParms = 10
		RETURN loBridge.InvokeStaticMethod(&amp;quot;System.String&amp;quot;,&amp;quot;Format&amp;quot;,lcFormat,lv1,lv2,lv3,lv4,lv5,lv6,lv7,lv8,lv9)
	CASE lnParms = 11
		RETURN loBridge.InvokeStaticMethod(&amp;quot;System.String&amp;quot;,&amp;quot;Format&amp;quot;,lcFormat,lv1,lv2,lv3,lv4,lv5,lv6,lv7,lv8,lv10)
	OTHERWISE
	    ERROR &amp;quot;Too many parameters for FormatString&amp;quot;
ENDCASE

ENDFUNC
*   StringFormat

************************************************************************
*  EscapeCSharpString
****************************************
FUNCTION EscapeCSharpString(lcValue)

lcValue = STRTRAN(lcValue, &amp;quot;\r&amp;quot;, CHR(13))
lcValue = STRTRAN(lcValue, &amp;quot;\n&amp;quot;, CHR(10))
lcValue = STRTRAN(lcValue, &amp;quot;\t&amp;quot;, CHR(9))
lcValue = STRTRAN(lcValue, &amp;quot;\0&amp;quot;, CHR(0))

RETURN lcValue
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first thing you notice here's is that we are calling a &lt;strong&gt;static method&lt;/strong&gt; on the &lt;code&gt;System.String&lt;/code&gt; class. Static methods are non-instance method, meaning you don't first create an instance. Instead the methods are static and bound to a specific type ie. &lt;code&gt;System.String&lt;/code&gt;. In FoxPro this is similar to a &lt;strong&gt;UDF()&lt;/strong&gt; function or plain function that is globally available. Static methods and properties are referenced by the type name - ie. &lt;code&gt;System.String&lt;/code&gt; instead of the instance, followed by the method or member name.&lt;/p&gt;
&lt;p&gt;Here we call the static Format method with the format string and a single value as a parameter:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;loBridge.InvokeStaticMethod(&amp;quot;System.String&amp;quot;,&amp;quot;Format&amp;quot;,lcFormat,lv1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this method, you'll notice the requirement to call each of the overloads for each parameter variation, which looks tedious but actually is the most efficient way to call this method. There are other overloads of &lt;code&gt;InvokeStaticMethod()&lt;/code&gt; that can be passed an array of parameters, and while that would be cleaner to look at and allow for an unlimited number of parameters, it's less efficient as the array has to be created and parsed on both ends. Passing values directly is significantly faster, and for a low-level utility method like this, it's definitely beneficial to optimize performance as much as possible.&lt;/p&gt;
&lt;h4 id="summary-1"&gt;Summary&lt;/h4&gt;
&lt;p&gt;In this example you learned:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Calling native .NET Methods&lt;/li&gt;
&lt;li&gt;Calling a non-instance Static method&lt;/li&gt;
&lt;li&gt;How .NET Format Strings and ToString() work&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="add-markdown-parsing-to-your-applications"&gt;Add Markdown Parsing to your Applications&lt;/h3&gt;
&lt;p&gt;&lt;small&gt;&lt;i&gt;&lt;/i&gt;&lt;/small&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Demonstrates:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Using a third party NuGet library&lt;/li&gt;
&lt;li&gt;Calling static methods&lt;/li&gt;
&lt;li&gt;Creating a FoxPro wrapper class to abstract functionality&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;The next example demonstrates using a Markdown to HTML parser. Markdown is a very useful text format that uses plain text mixed with a few readable text markup expressions that allow create rich HTML document text via plain text input. It can be used in lieu of WYIWYG editors and because it can be rendered very quickly allows you to actually preview content as you type in real time. So rather than typing in a simulated text editor to tries to simulate the final markup, you write plain text with markup simple expressions and look at a preview (or not) to see what the final output would look like.&lt;/p&gt;
&lt;p&gt;In short it's a great tool for writing text that needs to be a little more fancy than just a wall of plain text. It's super easy to add bold, italic, lists, notes, code snippets, embed link and images using Markdown.&lt;/p&gt;
&lt;h4 id="some-examples-of-markdown-usage"&gt;Some Examples of Markdown Usage&lt;/h4&gt;
&lt;p&gt;I'm a huge fan of Markdown and I've integrated it into several of my applications:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Markdown Monster (a Markdown Editor)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src="https://west-wind.com/wconnect/weblog/imageContent/2025/White-Paper/MarkdownMonster.png" style="max-width: 1000px" alt="Markdown Monster"&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Help Builder (HTML documentation and HTML preview)&lt;/li&gt;
&lt;li&gt;West Wind Message Board (used for message text)&lt;/li&gt;
&lt;li&gt;My Weblog - posts are written in Markdown and rendered to HTML&lt;/li&gt;
&lt;li&gt;Articles like this one - written in Markdown&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="using-markdig-for-markdown-to-html-conversion"&gt;Using Markdig for Markdown To HTML Conversion&lt;/h4&gt;
&lt;p&gt;Let's start with the simplest thing you can do which is to use a 3rd party library and it's most basic, default function to convert Markdown to Html which is sufficient for most use cases.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I'm using an older version of Markdig (&lt;code&gt;v0.15.2&lt;/code&gt;) because it has no extra dependencies. Later versions work fine (although the &lt;code&gt;ToHtml()&lt;/code&gt; method signature changes) but it requires that you add several additional dependencies of .NET assemblies. The old version has all the features you are likely to need so for FoxPro use this is the preferred version.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Notice that there are two parameters to the &lt;code&gt;Markdig.Markdown.ToHtml()&lt;/code&gt; method: The markdown and a markdown parser pipeline that is optional. Remember from FoxPro &lt;strong&gt;we always have to pass optional parameters&lt;/strong&gt; so we can pass the default value of &lt;code&gt;null&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;DO wwutils &amp;amp;&amp;amp; For Html Preview

do wwDotNetBridge
LOCAL loBridge as wwDotNetBridge
loBridge = GetwwDotnetBridge()

loBridge.LoadAssembly(&amp;quot;markdig.dll&amp;quot;)

TEXT TO lcMarkdown NOSHOW
# Raw Markdown Sample using the Markdig Parser

This is some sample Markdown text. This text is **bold** and *italic*.

* [Source Code for this sample on GitHub](https://github.com/../markdownTest.PRG)

![](https://markdownmonster.west-wind.com/docs/images/logo.png) 

* List Item 1
* List Item 2
* List Item 3

Great it works!

&amp;gt; #### Examples are great
&amp;gt; This is a block quote with a header
ENDTEXT


***  Actual Markdown Conversion here - Invoke a Static Method
lcHtml = loBridge.InvokeStaticMethod(&amp;quot;Markdig.Markdown&amp;quot;,&amp;quot;ToHtml&amp;quot;,;
                                     lcMarkdown,null)

? lcHtml
ShowHtml(lcHtml)  &amp;amp;&amp;amp; from wwUtils show in browser unformatted
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And that works:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://west-wind.com/wconnect/weblog/imageContent/2025/White-Paper/MarkdownTestFromFoxPro.png" alt="Markdown Test From Fox Pro"&gt;&lt;/p&gt;
&lt;p&gt;We have to load the &lt;code&gt;markdig.dll&lt;/code&gt; library, but the key feature of this code is the static method call to:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;lcHtml = loBridge.InvokeStaticMethod(&amp;quot;Markdig.Markdown&amp;quot;,&amp;quot;ToHtml&amp;quot;,;
                                     lcMarkdown,null)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This method takes the markdown to parse, plus a parameter of a ParserFactory which we have to pass even though the parameter is null. As I often do I first create the code I want to call in LinqPad to test, then call it from FoxPro. Here's the LinqPad test:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://west-wind.com/wconnect/weblog/imageContent/2025/White-Paper/MarkdigInLinqPad.png" alt="Markdig In Linq Pad"&gt;&lt;/p&gt;
&lt;p&gt;And that works.&lt;/p&gt;
&lt;h4 id="adding-a-more-sophisticated-parser-wrapper"&gt;Adding a more sophisticated Parser Wrapper&lt;/h4&gt;
&lt;p&gt;The call to &lt;code&gt;ToHtml()&lt;/code&gt; in its default form with the parser pipeline set to null gets you a default parser, but you might want to take advantage of additional features of add-ons that the parser supports. For example, you can add support for Github Flavored Markdown (Github specific features), Grid Tables, Pipe Tables, automatic link expansion and much more.&lt;/p&gt;
&lt;p&gt;To do this it's a good idea to create a wrapper class and build and cache the pipeline so it can be reused easily.&lt;/p&gt;
&lt;p&gt;Here's a Markdown Parser class:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;*************************************************************
DEFINE CLASS MarkDownParser AS Custom
*************************************************************
oPipeline = null
oBridge = null

lEncodeScriptBlocks = .T.
lSanitizeHtml = .T.
lNoHtmlAllowed = .F.

************************************************************************
FUNCTION Init()
****************************************
LOCAL loBridge as wwDotNetBridge

loBridge = GetwwDotnetBridge()

this.oBridge = loBridge
IF ISNULL(THIS.oBridge)
   RETURN .F.
ENDIF

IF !loBridge.LoadAssembly(&amp;quot;markdig.dll&amp;quot;)
   RETURN .F.
ENDIF   

ENDFUNC
*   Init

************************************************************************
FUNCTION CreateParser(llForce, llPragmaLines)
****************************************
LOCAL loBuilder, loValue, loBridge

IF llForce OR ISNULL(this.oPipeline)
	loBridge = this.oBridge
	loBuilder = loBridge.CreateInstance(&amp;quot;Markdig.MarkdownPipelineBuilder&amp;quot;)

	loValue = loBridge.Createcomvalue()
	loValue.SetEnum(&amp;quot;Markdig.Extensions.EmphasisExtras.EmphasisExtraOptions.Default&amp;quot;)	
	loBuilder = loBridge.Invokestaticmethod(&amp;quot;Markdig.MarkdownExtensions&amp;quot;,&amp;quot;UseEmphasisExtras&amp;quot;,loBuilder,loValue)

	loBuilder = loBridge.Invokestaticmethod(&amp;quot;Markdig.MarkdownExtensions&amp;quot;,&amp;quot;UseListExtras&amp;quot;,loBuilder)	
	loBuilder = loBridge.Invokestaticmethod(&amp;quot;Markdig.MarkdownExtensions&amp;quot;,&amp;quot;UseCustomContainers&amp;quot;,loBuilder)

	loBuilder = loBridge.Invokestaticmethod(&amp;quot;Markdig.MarkdownExtensions&amp;quot;,&amp;quot;UseFooters&amp;quot;,loBuilder)
	loBuilder = loBridge.Invokestaticmethod(&amp;quot;Markdig.MarkdownExtensions&amp;quot;,&amp;quot;UseFigures&amp;quot;,loBuilder)
	loBuilder = loBridge.Invokestaticmethod(&amp;quot;Markdig.MarkdownExtensions&amp;quot;,&amp;quot;UseFootnotes&amp;quot;,loBuilder)
	loBuilder = loBridge.Invokestaticmethod(&amp;quot;Markdig.MarkdownExtensions&amp;quot;,&amp;quot;UseCitations&amp;quot;,loBuilder)	
	
	loBuilder = loBridge.Invokestaticmethod(&amp;quot;Markdig.MarkdownExtensions&amp;quot;,&amp;quot;UsePipeTables&amp;quot;,loBuilder,null)
	loBuilder = loBridge.Invokestaticmethod(&amp;quot;Markdig.MarkdownExtensions&amp;quot;,&amp;quot;UseGridTables&amp;quot;,loBuilder)

	loValue = loBridge.Createcomvalue()
	loValue.SetEnum(&amp;quot;Markdig.Extensions.AutoIdentifiers.AutoIdentifierOptions.GitHub&amp;quot;)
	loBridge.Invokestaticmethod(&amp;quot;Markdig.MarkdownExtensions&amp;quot;,&amp;quot;UseAutoIdentifiers&amp;quot;,loBuilder,loValue)
	loBuilder = loBridge.Invokestaticmethod(&amp;quot;Markdig.MarkdownExtensions&amp;quot;,&amp;quot;UseAutoLinks&amp;quot;,loBuilder)
	
	loBuilder = loBridge.Invokestaticmethod(&amp;quot;Markdig.MarkdownExtensions&amp;quot;,&amp;quot;UseYamlFrontMatter&amp;quot;,loBuilder)
	loBuilder = loBridge.Invokestaticmethod(&amp;quot;Markdig.MarkdownExtensions&amp;quot;,&amp;quot;UseEmojiAndSmiley&amp;quot;,loBuilder,.T.)

	IF this.lNoHtmlAllowed
	   loBuilder = loBridge.Invokestaticmethod(&amp;quot;Markdig.MarkdownExtensions&amp;quot;,&amp;quot;DisableHtml&amp;quot;,loBuilder)
	ENDIF

	IF llPragmaLines
	  loBuiler = loBridge.Invokestaticmethod(&amp;quot;Markdig.MarkdownExtensions&amp;quot;,&amp;quot;UsePragmaLines&amp;quot;,loBuilder)
	ENDIF

	THIS.oPipeline = loBuilder.Build()
ENDIF

RETURN this.oPipeline
ENDFUNC
*   CreateParser

************************************************************************
FUNCTION Parse(lcMarkdown, llUtf8, llDontSanitizeHtml)
LOCAL lcHtml, loScriptTokens, loPipeline, lnOldCodePage

IF !this.lEncodeScriptBlocks
   loScriptTokens = TokenizeString(@lcMarkdown,&amp;quot;&amp;lt;%&amp;quot;,&amp;quot;%&amp;gt;&amp;quot;,&amp;quot;@@SCRIPT&amp;quot;)
ENDIF

loPipeline = this.CreateParser()

*** result always comes back as UTF-8 encoded
IF (llUtf8)
   lnOldCodePage = SYS(3101)
   SYS(3101,65001)
   lcMarkdown = STRCONV(lcMarkdown,9)
ENDIF

lcHtml = this.oBridge.InvokeStaticMethod(&amp;quot;Markdig.Markdown&amp;quot;,&amp;quot;ToHtml&amp;quot;,lcMarkdown,loPipeline)

IF llUtf8
  SYS(3101,lnOldCodePage)  
ENDIF

IF !THIS.lEncodeScriptBlocks
  lcHtml = DetokenizeString(lcHtml,loScriptTokens,&amp;quot;@@SCRIPT&amp;quot;)
ENDIF

IF PCOUNT() &amp;lt; 3
   llDontSanitizeHtml = !THIS.lSanitizeHtml
ENDIF   

IF !llDontSanitizeHtml
  lcHtml = THIS.SanitizeHtml(lcHtml)
ENDIF

lcHtml = TRIM(lcHtml,0,&amp;quot; &amp;quot;,CHR(13),CHR(10),CHR(9))

RETURN lcHTML   
ENDFUNC
*   Parse


************************************************************************
*  SanitizeHtml
****************************************
***  Function: Removes scriptable code from HTML. 
************************************************************************
FUNCTION SanitizeHtml(lcHtml, lcHtmlTagBlacklist)

IF EMPTY(lcHtmlTagBlackList)
	lcHtmlTagBlackList = &amp;quot;script|iframe|object|embed|form&amp;quot;
ENDIF
IF EMPTY(lcHtml)
   RETURN lcHtml	
ENDIF

RETURN THIS.oBridge.InvokeStaticMethod(&amp;quot;Westwind.WebConnection.StringUtils&amp;quot;,&amp;quot;SanitizeHtml&amp;quot;,lcHtml, lcHtmlTagBlacklist)
ENDFUNC
*   SanitizeHtml

ENDDEFINE
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The key method is the &lt;code&gt;CreateParser()&lt;/code&gt; which explicitly adds the features that we want to use with the parser. There are additional methods that help with optionally cleaning up HTML for safe rendering by removing script code and frames and other things that could allow XSS attacks against the rendered HTML as Markdown allows embedded HTML in the Markdown text.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;In the samples there's another sublass called &lt;code&gt;MarkdownParserExtended&lt;/code&gt; that adds a few more features to the parser that include code snippet parsing, expanding FontAwesomeIcons and a few other things. You can &lt;a href="https://github.com/RickStrahl/swfox2024-wwdotnetbridge-revisited/blob/master/markdownParser.PRG"&gt;look at the source code&lt;/a&gt; for more info.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;With this code in place you can now just create another helper method that uses this parser and cache it so we don't have to reload the pipeline and instance for each invocation:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;************************************************************************
*  Markdown
****************************************
***  Function: Converts Markdown to HTML
***    Assume: Caches instance in __MarkdownParser
***      Pass: lcMarkdown  - text to convert to HTML from Markdown
***            lnMode      - 0/.F. - standard, 2 extended, 1 - standard, leave scripts, 3 - extended leave scripts
***    Return: parsed HTML
************************************************************************
FUNCTION Markdown(lcMarkdown, lnMode, llReload, llUtf8, llNoSanitizeHtml, llNoHtmlAllowed)
LOCAL loMarkdown, lcClass

IF llReload OR VARTYPE(__MarkdownParser) != &amp;quot;O&amp;quot; 
	IF EMPTY(lnMode)
	   lnMode = 0
	ENDIF   

	lcClass = &amp;quot;MarkdownParser&amp;quot;
	IF lnMode = 2
	   lcClass = &amp;quot;MarkdownParserExtended&amp;quot;
	ENDIF
	
	loMarkdown = CREATEOBJECT(lcClass)
	PUBLIC __MarkdownParser
	__MarkdownParser = loMarkdown
	
	IF lnMode = 1 OR lnMode = 3
	   __MarkdownParser.lEncodeScriptBlocks = .F.  	  	   	  
	ENDIF	
	
	__MarkdownParser.lSanitizeHtml = !llNoSanitizeHtml
	__MarkdownParser.lNoHtmlAllowed = llNoHtmlAllowed
ELSE
    loMarkdown = __MarkdownParser
ENDIF

RETURN loMarkdown.Parse(lcMarkdown, llUtf8)
ENDFUNC
*   Markdown
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id="using-templates-to-make-the-markdown-look-nicer"&gt;Using Templates to make the Markdown Look Nicer&lt;/h4&gt;
&lt;p&gt;Markdown is useful &lt;strong&gt;especially in Web applications&lt;/strong&gt; where HTML can be directly displayed inside of a Web Page. But if you just generate the HTML and display it &lt;strong&gt;as is&lt;/strong&gt; the output is somewhat underwhelming as you're getting the browser's default styling.&lt;/p&gt;
&lt;p&gt;If you're using Markdown in desktop applications what you'd want to do, likely is to create an HTML page template into which to render the generated HTML, with CSS styling applied so you can produce output that looks a little more user friendly:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://west-wind.com/wconnect/weblog/imageContent/2025/White-Paper/MarkdownTemplateOutput.png" alt="Markdown Template Output"&gt;&lt;/p&gt;
&lt;p&gt;This actually uses styling I picked up from Markdown Monster via templating. This works by creating an HTML template and embedding the rendered markdown - along with some base paths - into it:&lt;/p&gt;
&lt;blockquote&gt;
&lt;h5 id="beware-of-foxpros-textmerge-function"&gt;Beware of FoxPro's TextMerge() Function&lt;/h5&gt;
&lt;/blockquote&gt;
&lt;p&gt;Initially I used the &lt;code&gt;TextMerge()&lt;/code&gt; function to merge text, but it turns out that it has difficulty with linefeeds (&lt;code&gt;/n&lt;/code&gt;) rather than CRLF, which are common with Markdown content created in external editors. For certain things like code snippets the stripped line breaks are causing problems. So rather than using &lt;code&gt;TextMerge()&lt;/code&gt; in the code below I'm explicitly placeholder values in the text.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-html"&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&amp;quot;en&amp;quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;base href=&amp;quot;${basePath}&amp;quot;/&amp;gt;
    &amp;lt;meta http-equiv=&amp;quot;content-type&amp;quot; content=&amp;quot;text/html; charset=utf-8&amp;quot; /&amp;gt;
    &amp;lt;meta charset=&amp;quot;utf-8&amp;quot;/&amp;gt;

    &amp;lt;meta http-equiv=&amp;quot;X-UA-Compatible&amp;quot; content=&amp;quot;IE=edge&amp;quot;/&amp;gt;
    &amp;lt;link href=&amp;quot;${themePath}..\Scripts\fontawesome\css\font-awesome.min.css&amp;quot; rel=&amp;quot;stylesheet&amp;quot;/&amp;gt;
    &amp;lt;link href=&amp;quot;${themePath}Theme.css&amp;quot; rel=&amp;quot;stylesheet&amp;quot;/&amp;gt;

    &amp;lt;!-- All this is for Code Snippet expansion --&amp;gt;
    &amp;lt;script src=&amp;quot;${themePath}..\Scripts\jquery.min.js&amp;quot;&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;link href=&amp;quot;${themePath}..\Scripts\highlightjs\styles\vs2015.css&amp;quot; rel=&amp;quot;stylesheet&amp;quot;/&amp;gt;
    &amp;lt;script src=&amp;quot;${themePath}..\Scripts\highlightjs\highlight.pack.js&amp;quot;&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;script src=&amp;quot;${themePath}..\Scripts\highlightjs-badge.js&amp;quot;&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;script src=&amp;quot;${themePath}..\Scripts\preview.js&amp;quot; id=&amp;quot;PreviewScript&amp;quot;&amp;gt;&amp;lt;/script&amp;gt; 
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
  &amp;lt;!-- Optional header for the page --&amp;gt;   
  &amp;lt;div style=&amp;quot;padding: 0.7em; background: #444; color: white; font-size: 0.8em&amp;quot;&amp;gt;    
    &amp;lt;div style=&amp;quot;float: right; color: goldenrod; font-weight: 600&amp;quot;&amp;gt;    
      Southwest Fox Conference, 2024
    &amp;lt;/div&amp;gt;
    &amp;lt;div&amp;gt;
      &amp;lt;img src=&amp;quot;Assets/touch-icon.png&amp;quot; style=&amp;quot;max-height: 1.3em; padding-right: 0.4em&amp;quot; /&amp;gt;
      &amp;lt;span style=&amp;quot;font-size: 1.35em; font-weight: 600; color: goldenrod; font-weight: 600&amp;quot;&amp;gt;wwdotnetbridge Revisited&amp;lt;/span&amp;gt; 
      &amp;lt;i style=&amp;quot;font-size: 0.7em&amp;quot;&amp;gt;by Rick Strahl&amp;lt;/i&amp;gt;
    &amp;lt;/div&amp;gt;    
  &amp;lt;/div&amp;gt;

&amp;lt;div id=&amp;quot;MainContent&amp;quot;&amp;gt;

  &amp;lt;!-- Markdown Monster Content --&amp;gt;
  
  ${htmlContent}
  
  &amp;lt;!-- End Markdown Monster Content --&amp;gt;
&amp;lt;/div&amp;gt;

&amp;lt;/body&amp;gt; 
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As you can see there are areas to be replaced with &lt;code&gt;${htmlContent}&lt;/code&gt; and &lt;code&gt;${basePath}&lt;/code&gt; and &lt;code&gt;${themePath}&lt;/code&gt; variables.&lt;/p&gt;
&lt;p&gt;I then use a &lt;code&gt;ShowWebPage.prg&lt;/code&gt; file to render the HTML into the template to get the desired styling and other feature support.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;LPARAMETERS lcHTML, lcFile, lcThemePath

************************************************************************
* FUNCTION ShowWebPage
**********************
***  Function: Takes an HTML string and displays it in the default
***            browser. 
***    Assume: Uses a file to store HTML temporarily.
***            For this reason there may be concurrency issues
***            unless you change the file for each use
***      Pass: lcHTML       -   HTML to display
***            lcFile       -   Temporary File to use (Optional)
***            loWebBrowser -   Web Browser control ref (Optional)
************************************************************************
LOCAL lcBasePath, lcThemePath

lcHTML=IIF(EMPTY(lcHTML),&amp;quot;&amp;quot;,lcHTML)
lcFile=IIF(EMPTY(lcFile),SYS(2023)+&amp;quot;\ww_HTMLView.htm&amp;quot;,lcFile)
lcBasePath = ADDBS(SYS(5) + CURDIR())
lcThemePath = lcBasePath + &amp;quot;Assets\Westwind\&amp;quot;
lcTemplate = &amp;quot;./Assets/Westwind/Theme.html&amp;quot;

IF !EMPTY(lcTemplate)
   lcT =  FILETOSTR(lcTemplate)

   *** TextMerge does weird shit with LineBreaks so do explicit replacements	
   * lcMerged = TEXTMERGE(lcT)
   
   *** For some reason TEXTMERGE strips line feeds
   lcT = STRTRAN(lcT,&amp;quot;${basePath}&amp;quot;, lcBasePath)
   lcT = STRTRAN(lcT,&amp;quot;${themePath}&amp;quot;, lcThemePath)
   lcHtml = STRTRAN(lcT, &amp;quot;${htmlContent}&amp;quot;, lcHtml)
ENDIF

*** Dump to file and preview in Browser
STRTOFILE(STRCONV(lcHtml,9),lcFile)
ShellExecute(lcFile)

RETURN


FUNCTION ShellExecute(tcUrl, tcAction, tcDirectory, tcParms, tnShowWindow)

IF VARTYPE(tnShowWindow) # &amp;quot;N&amp;quot;
   tnShowWindow = 1
ENDIF

IF EMPTY(tcUrl)
   RETURN -1
ENDIF
IF EMPTY(tcAction)
   tcAction = &amp;quot;OPEN&amp;quot;
ENDIF
IF EMPTY(tcDirectory)
   tcDirectory = SYS(2023) 
ENDIF

DECLARE INTEGER ShellExecute  ;
    IN SHELL32.dll as ShellExec_1;
    INTEGER nWinHandle,;
    STRING cOperation,;
    STRING cFileName,;
    STRING cParameters,;
    STRING cDirectory,;
    INTEGER nShowWindow
    
IF EMPTY(tcParms)
   tcParms = &amp;quot;&amp;quot;
ENDIF

RETURN ShellExec_1( _Screen.HWnd,;
                    tcAction,tcUrl,;
                    tcParms,tcDirectory,tnShowWindow)
ENDFUNC
*   ShellExecute
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To use this now becomes pretty simple:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;loParser = CREATEOBJECT(&amp;quot;MarkdownParserExtended&amp;quot;)

TEXT TO lcMarkdown NOSHOW
This is some sample Markdown text. This text is **bold** and *italic*.

* List Item 1
* List Item 2
* List Item 3

Great it works!

&amp;gt; #### &amp;lt;i class=&amp;quot;fas fa-info-circle&amp;quot; style=&amp;quot;font-size: 1.1em&amp;quot;&amp;gt;&amp;lt;/i&amp;gt;  Examples are great
&amp;gt; This is a block quote with a header


### Link Embedding is easy

* [Sample Repositor](https://github.com/RickStrahl/swfox2024-wwdotnetbridge-revisited)
* [Source Code for this sample on GitHub](https://github.com/RickStrahl/swfox2024-wwdotnetbridge-revisited/blob/master/markdownTest.PRG)

### Markdown Monster wants to eat your Markdown!

* [Download Markdown Monster](https://markdownmonster.west-wind.com)

![](https://markdownmonster.west-wind.com/Images/MarkdownMonsterLogo.jpg)

### The Markdown Editor for Windows

ENDTEXT

*** Render the page
lcHtml = loParser.Parse(lcMarkdown)
? lcHtml

*** 
ShowWebPage(lcHtml)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note that this example is hacked together using the existing Markdown Monster templates, so it it needs a little work to render quite right, but the idea is this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Set up a base HTML page&lt;/li&gt;
&lt;li&gt;Text Merge any paths and related things into the page&lt;/li&gt;
&lt;li&gt;Text Merge the rendered Html&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Have a fully reusable HTML template that you can easily call to render consistent looking Markdown. This is essentially what I do in Markdown Monster, Help Builder (both desktop apps) as well as on my Weblogs - all use Markdown text rendered into a base template.&lt;/p&gt;
&lt;p&gt;Markdown is an extremely powerful tool, and once you use it for a few things it becomes addicitive and you will loathe going back to a full editor like Word for most writing or documentation tasks.&lt;/p&gt;
&lt;h3 id="use-a-two-factor-authenticator-library"&gt;Use a Two-Factor Authenticator Library&lt;/h3&gt;
&lt;p&gt;&lt;small&gt;&lt;i&gt;&lt;/i&gt;&lt;/small&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Demonstrates:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use a third party library&lt;/li&gt;
&lt;li&gt;Make simple direct COM calls&lt;/li&gt;
&lt;li&gt;Very useful to create Two-Factor Auth for Web and Desktop apps&lt;/li&gt;
&lt;li&gt;NuGet Library used: &lt;a href="https://github.com/BrandonPotter/GoogleAuthenticator"&gt;GoogleAuthenticator&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;Two factor authentication is a second level of security that you can use &lt;strong&gt;in addition&lt;/strong&gt; to typical username/password or similar login style security both in Web and even in Desktop applications. Two-Factor auth serves a second unrelated safety mechanism to verify an account, which makes it much harder to hack an account even if passwords are compromised.&lt;/p&gt;
&lt;p&gt;There are many mechanisms that can be used for two-factor auth including using SMS messages to verify a user, or using a separate email address. However, a somewhat newer approach uses Authenticator apps like Authy, Google Authenticator, Microsoft Authenticator, or password managers like 1Password, Lastpass etc. The Authenticator is used as a &lt;strong&gt;key generator&lt;/strong&gt; that can then generate new one-time codes that can be checked. The two factor one time codes use a standard hashing mechanism that which means it works with any of the above mentioned tools.  All of these apps and devices uses the same algorithms to set up and then validate two factor one time codes. Any of these apps or devices can be used as Authenticators, but once you create a two-factor setup you have to use the same app or device to validate the codes.&lt;/p&gt;
&lt;p&gt;Two-Factor auth is fully self-contained which means there's no need to use a third party service or any special tools although you can use a hardware key as an option. Hardware keys provide their own fixed secret so you skip the typical QR/Setup code configuration and instead provide the secret key directly to be stored by the host application that will be validating the generated codes.&lt;/p&gt;
&lt;p&gt;Two factor authentication is made of two specific steps:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Setting up Two-Factor Authentication&lt;/strong&gt;&lt;br&gt;
This involves creating a setup key from a unique application generated secret key. The secret key is used to configure the Two Factor app, and it's used to create the initial QR/Setup code. For setup the code is typically displayed as a QR Code that can be scanned with a phone or from a Web app (in desktop Web browser). Alternately this code is also displayed as a numeric key you can manually type into an authenticator or hardware device where QR codes are not supported. To generate a new two-factor setup you typically provide some unique secret identifier that is then later also used to validate two-factor codes.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Validating a One Time Passkey&lt;/strong&gt;&lt;br&gt;
Once a Two-Factor auth setup has been configured, you then need to validate it. To validate you ask the Authenticator app for a new two-factor one time code and you validate it in combination with your unique application level secret identifier. Based on the two-factor one time code and and the identifier the code can be validated as valid within a given time frame.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here's what the sample looks like:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://west-wind.com/wconnect/weblog/imageContent/2025/White-Paper/TwoFactorQrCode.png" alt="Two Factor Qr Code"&gt;&lt;/p&gt;
&lt;p&gt;What I'm presenting here are the &lt;strong&gt;tools to do two-factor&lt;/strong&gt; authentication, not an actual implementation as that's way too complex to tackle here. If you are interested in full two-factor implementation for a Web site see my &lt;a href="https://weblog.west-wind.com/posts/2023/May/17/Implementing-TwoFactor-Auth-using-an-Authenticator-app-in-ASPNET"&gt;post on Two-Factor authentication in a .NET Web application&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;In a live application here's what this looks like.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;QR Code Setup&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://west-wind.com/wconnect/weblog/imageContent/2025/White-Paper/TwoFactor-Setup-WebApp.png" alt="TwoFactor Setup WebApp"&gt;&lt;/p&gt;
&lt;p&gt;As a user you can capture the QR code in an authenticator app like Authy here:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://west-wind.com/wconnect/weblog/imageContent/2025/White-Paper/Two-Factor-Authy.jpg" alt="Two Factor Authy"&gt;&lt;/p&gt;
&lt;p&gt;or, more conveniently as part of a password manager like 1Password as I do:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://west-wind.com/wconnect/weblog/imageContent/2025/White-Paper/Two-Factor-ScanQr-1Password.png" alt="Two Factor ScanQr 1Password"&gt;&lt;/p&gt;
&lt;p&gt;App presents a QR code to initially set up two factor authentication. This is a one time operation where the app generates a new secret key and then stores that secret key with the user/customer record to later use for validation.&lt;/p&gt;
&lt;p&gt;Then when a user logs in, they first log in with their username and password, and then are immediately asked for the two-factor one time code as well.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://west-wind.com/wconnect/weblog/imageContent/2025/White-Paper/TwoFactorValidationInWeb.png" alt="Two Factor Validation In Web"&gt;&lt;/p&gt;
&lt;p&gt;And you can fill that in via your authenticator app, or as I do here with 1Password:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://west-wind.com/wconnect/weblog/imageContent/2025/White-Paper/TwoFactor-1Password-Validation.png" alt="TwoFactor 1Password Validation"&gt;&lt;/p&gt;
&lt;h4 id="two-factor-auth-with-the-googleauthentor-library"&gt;Two-Factor Auth with the GoogleAuthentor Library&lt;/h4&gt;
&lt;p&gt;Unlike the name suggests, this library is not limited to Google Authenticator. It works with any two-factor authenticator like Authy, Microsoft Authenticator and 1Password. The algorithm that is used for two-factor auth hashing is generic so it works with any tool that supports these protocols, but once set up the codes can only be generated by your configured Authenticator tool.&lt;/p&gt;
&lt;p&gt;The code to use this library from FoxPro is very simple. The easiest way to use it is to create a small wrapper class:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;*************************************************************
DEFINE CLASS TwoFactorAuthentication AS Custom
*************************************************************

oBridge = null
oAuth = null

************************************************************************
FUNCTION Init()
****************************************
this.oBridge = GetwwDotnetBridge()

IF (!this.oBridge.LoadAssembly(&amp;quot;Google.Authenticator.dll&amp;quot;))
   ERROR this.oBridge.cErrorMsg
ENDIF  
THIS.oAuth = this.oBridge.CreateInstance(&amp;quot;Google.Authenticator.TwoFactorAuthenticator&amp;quot;)
IF VARTYPE(THIS.oAuth) # &amp;quot;O&amp;quot;
   ERROR &amp;quot;Failed to load TwoFactorAuthenticator: &amp;quot; + this.oBridge.cErrorMsg
ENDIF

ENDFUNC

*********************************************************************************
FUNCTION GenerateSetupCode(lcApplicationName, lcEmail, lcSecretKey, lnResolution)
****************************************
***  Function: Generates a structure that generates an object containing 
***            a QR code image and manual setup code
***    Assume: Application and Email have no effect on code/qr generation
***      Pass: lcApplicationName  - Name of application
***            lcEmail            - An email address to identify user
***            lcSecretKey        - Secret key tied to the user to identify
***            lnResolution       - larger numbers result in larger CR codes (10)
***    Return: TwoFactorSetup object or null
************************************************************************
LOCAL loAuth

IF EMPTY(lnResolution)
   lnResolution = 10
ENDIF   

loSetupInfo = THIS.oAuth.GenerateSetupCode(lcApplicationName,;
   lcEmail, ;
   lcSecretKey, ;
   .F., lnResolution)
   
loResult = CREATEOBJECT(&amp;quot;TwoFactorSetup&amp;quot;)
loResult.cQrCodeImageData = loSetupInfo.QrCodeSetupImageUrl
loResult.cSetupKey  = loSetupInfo.ManualEntryKey
loResult.cCustomerSecret = lcSecretKey

RETURN loResult
ENDFUNC

************************************************************************
FUNCTION ValidatePin(lcSecretKey, lcPin)
****************************************
lcPin = STRTRAN(lcPin, &amp;quot; &amp;quot; ,&amp;quot;&amp;quot;)
RETURN THIS.oAuth.ValidateTwoFactorPIN(lcSecretKey, lcPin)
ENDFUNC

ENDDEFINE


*************************************************************
DEFINE CLASS TwoFactorSetup AS Custom
*************************************************************

*** Base64 Data Url that contains the image data 

cQrCodeImageData = &amp;quot;&amp;quot;


*** Typable Version of the QrCode data
cSetupKey = &amp;quot;&amp;quot;

*** Unique Customer Key - pass and save with your app
cCustomerSecret = &amp;quot;&amp;quot;

************************************************************************
FUNCTION QrCodeHtml(llImageOnly)
****************************************
IF (llImageOnly)
   RETURN [&amp;lt;img src=&amp;quot;] + this.cQrCodeImageData + [&amp;quot; /&amp;gt;]
ENDIF

TEXT TO lcHtml NOSHOW TEXTMERGE
&amp;lt;html&amp;gt;
&amp;lt;body&amp;gt;
&amp;lt;div style=&amp;quot;text-align: center; max-width: 500px&amp;quot;&amp;gt;
	&amp;lt;img src=&amp;quot;&amp;lt;&amp;lt;this.cQrCodeImageData&amp;gt;&amp;gt;&amp;quot; /&amp;gt;
	&amp;lt;div style=&amp;quot;font-size: 1.5em; font-weight: 600&amp;quot;&amp;gt;
	&amp;lt;&amp;lt;this.cSetupKey&amp;gt;&amp;gt;
	&amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
ENDTEXT

RETURN lcHtml
* QrCodeHtml

ENDDEFINE
*EOC TwoFactorSetup
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The class only has two methods that wrap the basic behavior to create a new Two-Factor setup and to validate it.&lt;/p&gt;
&lt;h4 id="generating-and-displaying-a-qr-setup-code"&gt;Generating and Displaying a QR Setup Code&lt;/h4&gt;
&lt;p&gt;Let's start with how to generate and then display the QR Code:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;*** For demo only (ShowHtml()/InputForm()/GetUniqueId())
DO wwutils

*** Generate a Customer Secret key 
*** In an application this will be something that identifies the user
*** with a given account, but it needs to be something that is not known
*** typically a generated UniqueId stored in a customer record (TwoFactorKey in db for example)
lcSecret = GetUniqueId(12)    

loAuth = CREATEOBJECT(&amp;quot;TwoFactorAuthentication&amp;quot;)
loSetup  = loAuth.GenerateSetupCode(&amp;quot;Westwind.Webstore&amp;quot;,;
                                    &amp;quot;rick@test-my-site.com&amp;quot;, lcSecret)

ShowHtml( loSetup.QrCodeHtml())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Yeah pretty simple, eh? To start you need some sort of unique ID that you need to store with your application once the two-factor authentication has been enabled. This key is the shared secret between you and the authenticator and it's needed to validate two-factor codes. Ideally you want a random value for this and store it as a separate field in your user or customer database that identifies a user.&lt;/p&gt;
&lt;p&gt;You call &lt;code&gt;GenerateSetupCode()&lt;/code&gt; to generate the QrCode or manual setup code which are used initialize the Authenticator for your app.&lt;/p&gt;
&lt;p&gt;The method returns an &lt;code&gt;loSetup&lt;/code&gt; structure that contains the &lt;code&gt;cQrCodeImageData&lt;/code&gt; which is HTML base64 encoded image data that can be directly assigned to an &lt;code&gt;&amp;lt;img src=&amp;quot;&amp;quot; /&amp;gt;&lt;/code&gt; attribute. We can take advantage of that to display the QR code quite easily in an HTML page as shown in the initial examples.&lt;/p&gt;
&lt;p&gt;The Setup class includes a mechanism to render the image as HTML into a page that can be displayed:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://west-wind.com/wconnect/weblog/imageContent/2025/White-Paper/Two-Factor-QrCodeRenderedInBrowser.png" alt="Two Factor QrCodeRenderedInBrowser"&gt;&lt;/p&gt;
&lt;p&gt;You can see the image tag:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-html"&gt;&amp;lt;img src=&amp;quot;data:image/png;base64,&amp;lt;longBase64Text&amp;gt;&amp;quot;  /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Alternately if you want to capture the actual image data you can parse the encoded &lt;code&gt;cQrCodeImageData&lt;/code&gt;, extract the base64 text and use &lt;code&gt;STRCONV()&lt;/code&gt; to turn that into binary data that you can display in an image control which would also work.&lt;/p&gt;
&lt;p&gt;The idea with this method is that you do this for setting up your authenticator from the QR code or setup code.&lt;/p&gt;
&lt;p&gt;As part of the setup process you'll also need to verify that it's working &lt;strong&gt;before you enable two-factor authentication&lt;/strong&gt;. So a typical Web Form will ask to scan the QR Code and then also provide an initial one time code to verify that it works. Only after that code has been verified should you actually enable two-Factor auth in your application.&lt;/p&gt;
&lt;h4 id="validating-a-two-factor-one-time-code"&gt;Validating a Two-Factor One-Time Code&lt;/h4&gt;
&lt;p&gt;Once Two-Factor auth has been enabled, you can now log in with your username/password auth and then immediately also check the two factor auth.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;loAuth = CREATEOBJECT(&amp;quot;TwoFactorAuthentication&amp;quot;)

*** Test with a 'known' Customer Secret Key (from my site's test account)
*** In a real app you'd retrieve this from a user/customer record
lcSecret = GETENV(&amp;quot;TWOFACTOR_SECRETKEY&amp;quot;)

*** Capture Pin (lookup in 1Password, Authy, Google or Microsoft Authenticator, Yubikey etc.
lcPin = InputForm(&amp;quot;&amp;quot;,&amp;quot;Enter generated two-factor pin&amp;quot;)
IF EMPTY(lcPin)
   RETURN
ENDIF

*** Check the One Time Code
If(loAuth.ValidatePin(lcSecret,lcPin))
   ? &amp;quot;Pin has been validated&amp;quot;
ELSE
   ? &amp;quot;Invalid Pin code&amp;quot;   
ENDIF
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So then I can use my matching user account that matches the secret key, and now use the generated one-time code out of 1Password:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://west-wind.com/wconnect/weblog/imageContent/2025/White-Paper/Two-Factor-Validating-In-Desktop.png" alt="Two Factor Validating In Desktop"&gt;&lt;/p&gt;
&lt;p&gt;I can paste the value and the Authenticator checks for validity.&lt;/p&gt;
&lt;h4 id="two-factoring-logic-is-up-to-you"&gt;Two-Factoring: Logic is up to you&lt;/h4&gt;
&lt;p&gt;The code I've shown provides the logistics, but how you implement it is up to you. You can use Two-Factor auth in Web apps where it's quite common, but as you've seen here it's also possible to do this in Desktop applications as long as you can display a QR code - or you can optionally just use the Manual Setup code.&lt;/p&gt;
&lt;h3 id="add-spellchecking-to-your-applications"&gt;Add Spellchecking to your applications&lt;/h3&gt;
&lt;p&gt;&lt;small&gt;&lt;i&gt;&lt;/i&gt;&lt;/small&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Demonstrates:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Using a third party library&lt;/li&gt;
&lt;li&gt;Dealing with Assembly Version Mismatches&lt;/li&gt;
&lt;li&gt;Using a static Factory method&lt;/li&gt;
&lt;li&gt;Using instance methods&lt;/li&gt;
&lt;li&gt;Creating a wrapper class&lt;/li&gt;
&lt;li&gt;Using a Generic List&lt;/li&gt;
&lt;li&gt;Wrapping a .NET Collection with FoxPro Collection&lt;/li&gt;
&lt;li&gt;Checking text spelling and generating spelling suggestsions&lt;/li&gt;
&lt;li&gt;NuGet Library used: [WeCantSpell.HUnspell]https://github.com/aarondandy/WeCantSpell.Hunspell&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;Spellchecking is a useful feature if you're dealing with text applications and you can easily integrate a library that can check for misspelled words and suggests correctly spelled words on a misspelling.&lt;/p&gt;
&lt;p&gt;Again, I'm not going to provide an integrated solution here, but just the tools to access this functionality. For a complete solution you likely need additional text parsing of larger blocks of text that looks at each word and then somehow highlights each misspelled word and displays a dropdown of suggestions.&lt;/p&gt;
&lt;p&gt;Here's an implementation in &lt;a href="https://helpbuilder.west-wind.com/"&gt;Westwind Html Help Builder&lt;/a&gt; (a FoxPro application using an HTML based Markdown editor interface):&lt;/p&gt;
&lt;p&gt;&lt;img src="https://west-wind.com/wconnect/weblog/imageContent/2025/White-Paper/SpellCheck-HighlightAndSuggest.png" alt="SpellCheck Highlight and Suggest"&gt;&lt;/p&gt;
&lt;p&gt;The editor parses each paragraph for individual words and then feeds each words into the spell checker in FoxPro. If a word is not spelled correctly the editor highlights the word, and on right click requests a list of suggestions that are then displayed in the drop down.&lt;/p&gt;
&lt;h4 id="spell-checking-with-wecantspellhunspell"&gt;Spell Checking with WeCantSpell.Hunspell&lt;/h4&gt;
&lt;p&gt;The interface to this library is pretty straight forward:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A static factory method initializes a 'word list'&lt;/li&gt;
&lt;li&gt;The word list is used to check spelling&lt;/li&gt;
&lt;li&gt;The word list is used to provide a list of suggestions&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A good way to start is to look at LinqPad to see what the .NET code looks like which is often cleaner and helps with figuring out what the exact types are:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://west-wind.com/wconnect/weblog/imageContent/2025/White-Paper/Spellcheck-LinqPad.png" alt="Spellcheck LinqPad"&gt;&lt;/p&gt;
&lt;p&gt;One thing that jumps out here is that the list of suggestions is returned as a &lt;code&gt;IList&amp;lt;string&amp;gt;&lt;/code&gt; result. This is a &lt;strong&gt;generic list&lt;/strong&gt; result which is significant in that generic types are not directly supported in COM or FoxPro and can't be passed back into FoxPro. So we know that the call to the &lt;code&gt;Suggest()&lt;/code&gt; method will have to be an indirect call via &lt;code&gt;loBridge.InvokeMethod()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Let's look at the mapped FoxPro code:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;loBridge = GetwwDotnetBridge()

loBridge.LoadAssembly(&amp;quot;WeCantSpell.Hunspell.dll&amp;quot;)

*** Location of Dictionary Files *.dic and *.aff
dictFolder = FULLPATH(&amp;quot;bin\&amp;quot;)

*** Creates a Spellchecker instance by loading dictionary and grammar files
loSpell = loBridge.InvokeStaticMethod(&amp;quot;WeCantSpell.Hunspell.WordList&amp;quot;,;
                                    &amp;quot;CreateFromFiles&amp;quot;,;
                                    dictFolder +&amp;quot;en_US.dic&amp;quot;,;
                                    dictFolder + &amp;quot;en_US.aff&amp;quot;)

? &amp;quot;*** Check 'Colour' (en_US)&amp;quot;
? loSpell.Check(&amp;quot;Colour&amp;quot;)   &amp;amp;&amp;amp; false
?
? &amp;quot;*** Check 'Color' (en_US)&amp;quot;
? loSpell.Check(&amp;quot;Color&amp;quot;)   &amp;amp;&amp;amp; true
?

? &amp;quot;*** Suggestions for misspelled 'Colour'&amp;quot;
loSuggestions = loBridge.InvokeMethod(loSpell,&amp;quot;Suggest&amp;quot;,&amp;quot;Colour&amp;quot;)

*** loSuggestions is a  `ComArray` Instance (IList&amp;lt;string&amp;gt;)
? loSuggestions.Count

*** Iterate over array items
FOR lnX = 0 TO loSuggestions.Count -1
   ? loSuggestions.Item(lnX)
ENDFOR
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And that works just fine. We can call &lt;code&gt;.Check()&lt;/code&gt; directly because it's a very simple method with simple parameters. The &lt;code&gt;.Suggest()&lt;/code&gt; method returns a list and in order to access collection values and especially generic list values, we need to use &lt;code&gt;InvokeMethod()&lt;/code&gt; to execute the code from within .NET. The result is returned as a &lt;code&gt;ComArray&lt;/code&gt; instance, and we can use the &lt;code&gt;.Count&lt;/code&gt; and &lt;code&gt;.Item()&lt;/code&gt; method to retrieve the individual values.&lt;/p&gt;
&lt;h4 id="wrapper-class-makes-it-easier"&gt;Wrapper Class Makes it easier&lt;/h4&gt;
&lt;p&gt;Although the above code is easy enough to use, it's really a good idea to abstract this functionality into a class. You don't want to have to remember how to make the two &lt;code&gt;InvokeMethod()&lt;/code&gt; calls and then deal with the &lt;code&gt;ComArray&lt;/code&gt; instance. Instead we could create a class that automatically initializes the wordlist and then stores it internally. We can implement a &lt;code&gt;.Spell()&lt;/code&gt; method that simply passes through, but for the &lt;code&gt;.Suggest()&lt;/code&gt; method we can perhaps turn the &lt;code&gt;ComArray&lt;/code&gt; into a native FoxPro Collection.&lt;/p&gt;
&lt;p&gt;Here's what that looks like:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;*************************************************************
DEFINE CLASS HunspellChecker AS Custom
*************************************************************

oBridge = null
oSpell = null
cLanguage = &amp;quot;en_US&amp;quot;
cDictionaryFolder = &amp;quot;&amp;quot; &amp;amp;&amp;amp; root

************************************************************************
FUNCTION init(lcLang, lcDictFolder)
****************************************
IF EMPTY(lcLang)
   lcLang = this.cLanguage
ENDIF
IF EMPTY(lcDictFolder)
   lcDictFolder = this.cDictionaryFolder
ENDIF
   
this.oBridge = GetwwDotnetBridge()
IF ISNULL(this.oBridge)
      ERROR &amp;quot;Failed to load HUnspell: &amp;quot; + this.oBridge.cErrorMsg
ENDIF

LOCAL loBridge as wwDotNetBridge
loBridge = GetwwDotnetBridge()
if(!loBridge.LoadAssembly(&amp;quot;WeCantSpell.Hunspell.dll&amp;quot;))
  ERROR &amp;quot;Failed to load WeCantSpell.Hunspell.dll: &amp;quot; + this.oBridge.cErrorMsg
ENDIF

IF !EMPTY(lcDictFolder)
	lcDictFolder = ADDBS(lcDictFolder)
ELSE
    lcDictFolder = &amp;quot;&amp;quot;
ENDIF

THIS.oSpell = loBridge.InvokeStaticMethod(&amp;quot;WeCantSpell.Hunspell.WordList&amp;quot;,;
                                    &amp;quot;CreateFromFiles&amp;quot;,;
                                  lcDictFolder + lcLang + &amp;quot;.dic&amp;quot;,;
                                  lcDictFolder + lcLang + &amp;quot;.aff&amp;quot;)

IF ISNULL(this.oSpell)
  ERROR &amp;quot;Failed to load HUnspell: &amp;quot; + this.oBridge.cErrorMsg
ENDIF

ENDFUNC
*   init

************************************************************************
FUNCTION Spell(lcWord)
****************************************
LOCAL llResult

IF ISNULLOREMPTY(lcWord) OR LEN(lcWord) = 1
   RETURN .T.
ENDIF

llResult = this.oSpell.Check(lcWord)

RETURN llResult
ENDFUNC
*   Spell

************************************************************************
FUNCTION Suggest(lcWord)
****************************************
LOCAL loWords, lnX

loCol = CREATEOBJECT(&amp;quot;collection&amp;quot;)

*** Returns a Collection of values (not an array)
loWords = this.oBridge.InvokeMethod(this.oSpell,&amp;quot;Suggest&amp;quot;,lcWord)

lnCount = loWords.Count

FOR lnX = 0 TO lnCount -1
    *** return indexed value (0 based) from the list collection
    lcWord = loWords.Item(lnX)
    loCol.Add( lcWord )
ENDFOR


RETURN loCol
ENDFUNC
*   Suggest

************************************************************************
FUNCTION Destroy()
****************************************

*** MUST dispose to release memory for spell checker
*this.oSpell.Dispose()
this.oSpell = null

ENDFUNC
*   Destroy

ENDDEFINE
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This makes it much easier to create an instances of the class and keep it around for the duration of the application. You'll want to minimize loading this class as loading the dictionary from disk initially can be on the slow side (takes a second or two).&lt;/p&gt;
&lt;p&gt;Beyond that the &lt;code&gt;Suggest()&lt;/code&gt; method returns a cleaner FoxPro collection that you can &lt;code&gt;FOR EACH&lt;/code&gt; over and use with more familiar 1 based array logic.&lt;/p&gt;
&lt;p&gt;Using this class the previous code gets a little simpler yet:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;*** Using the Wrapper Class
CLEAR

*** Loads library and dictionary - you can cache this for reuse
loSpell = CREATEOBJECT(&amp;quot;HunspellChecker&amp;quot;,&amp;quot;en_US&amp;quot;,&amp;quot;.\bin&amp;quot;)   


? &amp;quot;*** Check spelling for:&amp;quot;
? &amp;quot;Testing: &amp;quot; + TRANSFORM( loSpell.Spell(&amp;quot;Testing&amp;quot;)  )

? &amp;quot;Tesdting: &amp;quot; + TRANSFORM(loSpell.Spell(&amp;quot;Tesdting&amp;quot;)      )


lcWord = &amp;quot;aren'tt&amp;quot;
? &amp;quot;Suggestions for &amp;quot; + lcWord
loSug = loSpell.Suggest(lcWord)
? loSug.Count
FOR EACH lcWord in loSug
  ? lcWord
ENDFOR
loSpell = null

loSpell = CREATEOBJECT(&amp;quot;HunspellChecker&amp;quot;,&amp;quot;de_DE&amp;quot;,&amp;quot;.\bin&amp;quot;)

? &amp;quot;*** Using German Dictionary:&amp;quot;
? &amp;quot;Zahn: &amp;quot; + TRANSFORM(loSpell.Spell(&amp;quot;Zahn&amp;quot;))
? &amp;quot;Zahnn: &amp;quot; + TRANSFORM(loSpell.Spell(&amp;quot;Zahnn&amp;quot;))     
? &amp;quot;ZÃ¤hne: &amp;quot; + TRANSFORM(loSpell.Spell(&amp;quot;ZÃ¤hne&amp;quot;))  
? &amp;quot;lÃ¤uft: &amp;quot; + TRANSFORM(loSpell.Spell(&amp;quot;lÃ¤uft&amp;quot;))

? &amp;quot;***  Suggest for ZÃ¤hjne&amp;quot;
loSug = loSpell.Suggest(&amp;quot;ZÃ¤hjne&amp;quot;)
FOR EACH lcWord in loSug
  ? lcWord
ENDFOR
     
? loSug.Count
loSpell = null


? &amp;quot;*** Text to check:&amp;quot;
TEXT TO lcText
These ae somme of the worsd that are badly mispeled.

I cannot imaggine that somebody can spel so bad.

ENDTEXT
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note that you can spell check for different languages by applying the appropriate dictionary files. HUnspell uses standard dictionary files that are also used by OpenOffice and you can download other languages from there.&lt;/p&gt;
&lt;p&gt;Finally, here's a rudimentary implementation that lets you get all misspelled words in a paragraph or other lengthy block of text and then also return the suggestions for the misspelled words:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;loSpell = CREATEOBJECT(&amp;quot;HunspellChecker&amp;quot;,&amp;quot;en_US&amp;quot;,&amp;quot;.\bin&amp;quot;)   
loWords = GetWords(lcText)

LOCAL lnX
? &amp;quot;*** Mispelled words:&amp;quot;
FOR lnX = 1 TO loWords.Count   
   lcWord = loWords.Item(lnX)
   lcSuggest = &amp;quot;&amp;quot;

   IF (!loSpell.Spell(lcWord))
      loSug = loSpell.Suggest(lcWord)
      IF (loSug.Count &amp;gt; 0)
      	  
          FOR lnY = 1 TO loSug.Count
			lcSuggest = lcSuggest + loSug.Item(lnY) + &amp;quot; &amp;quot;
	   	  ENDFOR
	  ENDIF
	  
	  ? lcWord + &amp;quot; - &amp;quot; + lcSuggest   
	ENDIF
	
ENDFOR
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;From here you can build an interactive solution to integrate spellchecking into your own applications.&lt;/p&gt;
&lt;h4 id="a-library-with-dependency-issues"&gt;A Library with Dependency Issues&lt;/h4&gt;
&lt;p&gt;It turns out this library is distributes as a .NET Standard library which means that it's not specifically targeted at .NET Framework (ie. 4.72 or later) but rather uses a more generic target. It also takes advantage of some newer .NET features that are not part of the core framework and require additional dependencies.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://west-wind.com/wconnect/weblog/imageContent/2025/White-Paper/Spellcheck-Libraries.png" alt="Spellcheck Libraries"&gt;&lt;/p&gt;
&lt;p&gt;As you can see there are several extra libraries that have to be distributed. That's easy enough, however it turns out that other applications also have dependencies on these same libraries and require slightly different versions.&lt;/p&gt;
&lt;p&gt;When this happens it's necessary to set up the appropriate assembly redirects that roll up to the latest version of each library. To do this you have to edit the following files:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Yourapp.exe.config&lt;/li&gt;
&lt;li&gt;vfp9.exe.config  (in the VFP9 install folder)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class="language-xml"&gt;&amp;lt;?xml version=&amp;quot;1.0&amp;quot;?&amp;gt;
&amp;lt;configuration&amp;gt;
  &amp;lt;startup&amp;gt;   
	&amp;lt;supportedRuntime version=&amp;quot;v4.0&amp;quot; sku=&amp;quot;.NETFramework,Version=v4.8&amp;quot; /&amp;gt;	
  &amp;lt;/startup&amp;gt;
  &amp;lt;runtime&amp;gt;
    &amp;lt;loadFromRemoteSources enabled=&amp;quot;true&amp;quot;/&amp;gt;
      
    &amp;lt;assemblyBinding xmlns=&amp;quot;urn:schemas-microsoft-com:asm.v1&amp;quot;&amp;gt;
      &amp;lt;dependentAssembly&amp;gt;
            &amp;lt;assemblyIdentity name=&amp;quot;Newtonsoft.Json&amp;quot; publicKeyToken=&amp;quot;30ad4fe6b2a6aeed&amp;quot; culture=&amp;quot;neutral&amp;quot; /&amp;gt;
            &amp;lt;bindingRedirect oldVersion=&amp;quot;0.0.0.0-13.0.0.0&amp;quot; newVersion=&amp;quot;13.0.0.0&amp;quot; /&amp;gt;
            &amp;lt;/dependentAssembly&amp;gt;
            &amp;lt;dependentAssembly&amp;gt;
            &amp;lt;assemblyIdentity name=&amp;quot;System.Runtime.CompilerServices.Unsafe&amp;quot; publicKeyToken=&amp;quot;b03f5f7f11d50a3a&amp;quot; culture=&amp;quot;neutral&amp;quot; /&amp;gt;
            &amp;lt;bindingRedirect oldVersion=&amp;quot;0.0.0.0-6.0.0.0&amp;quot; newVersion=&amp;quot;6.0.0.0&amp;quot; /&amp;gt;
            &amp;lt;/dependentAssembly&amp;gt;
            &amp;lt;dependentAssembly&amp;gt;
            &amp;lt;assemblyIdentity name=&amp;quot;System.Memory&amp;quot; publicKeyToken=&amp;quot;cc7b13ffcd2ddd51&amp;quot; culture=&amp;quot;neutral&amp;quot; /&amp;gt;
            &amp;lt;bindingRedirect oldVersion=&amp;quot;0.0.0.0-4.1.0.0&amp;quot; newVersion=&amp;quot;4.0.1.2&amp;quot; /&amp;gt;
            &amp;lt;/dependentAssembly&amp;gt;
            &amp;lt;dependentAssembly&amp;gt;
            &amp;lt;assemblyIdentity name=&amp;quot;System.Buffers&amp;quot; publicKeyToken=&amp;quot;cc7b13ffcd2ddd51&amp;quot; culture=&amp;quot;neutral&amp;quot; /&amp;gt;
            &amp;lt;bindingRedirect oldVersion=&amp;quot;0.0.0.0-4.1.0.0&amp;quot; newVersion=&amp;quot;4.0.3.0&amp;quot; /&amp;gt;
       &amp;lt;/dependentAssembly&amp;gt;
     &amp;lt;/assemblyBinding&amp;gt;     
     
  &amp;lt;/runtime&amp;gt;
  
&amp;lt;/configuration&amp;gt;  
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;It's important to note that the versions in the assembly bindings are &lt;strong&gt;Assembly Versions&lt;/strong&gt; &lt;em&gt;not File Versions&lt;/em&gt;. You can find assembly versions in tools like ILSpy by looking at each assembly and looking at the metadata for the assembly.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://west-wind.com/wconnect/weblog/imageContent/2025/White-Paper/ILSpyVersionNumber.png" alt="IL Spy Version Number"&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="humanize-numbers-dates-measurements"&gt;Humanize numbers, dates, measurements&lt;/h3&gt;
&lt;p&gt;&lt;small&gt;&lt;i&gt;&lt;/i&gt;&lt;/small&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Demonstrates:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Using a third party library&lt;/li&gt;
&lt;li&gt;Creating a Wrapper Class in .NET to avoid type complexity&lt;/li&gt;
&lt;li&gt;Create a Wrapper class in FoxPro for ease of use&lt;/li&gt;
&lt;li&gt;Work with Extensions Methods in .NET&lt;/li&gt;
&lt;li&gt;Nuget Library: &lt;a href="https://github.com/Humanizr/Humanizer"&gt;Humanizer&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;In this example, we'll see an example of creating a custom .NET DLL of our own and calling it from FoxPro. Humanizer is a cool library for text formatting that - as the name suggests - provides friendly naming for many common types. For example, things like using &lt;em&gt;Today&lt;/em&gt; or &lt;em&gt;10 days ago&lt;/em&gt; for dates, turning numbers into strings, pluralizing text, camel or snake casing un-casing of text, turning bytes into friendly descriptions and so on.&lt;/p&gt;
&lt;h4 id="examples-of-humanizing-number-dates-words-sizes-and-more"&gt;Examples of Humanizing Number, Dates, Words, Sizes, and more&lt;/h4&gt;
&lt;p&gt;What's different with this example and why I prefer to delegate access of features to a .NET wrapper class is that Humanizer implements most of its functionality as extension methods and it uses a lot of nullable types. It's possible to access these features through wwDotnetBridge, but it's just significantly easier to do from within .NET and it's possible to create a very simple .NET class that exposes the features we want directly to FoxPro without the need of a FoxPro wrapper class.&lt;/p&gt;
&lt;p&gt;To demonstrate some of the features of Humanizer and the functions I've exposed here's an example of the FoxPro code that uses the .NET wrapper component first:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;loBridge = GetwwDotnetBridge()
loBridge.LoadAssembly(&amp;quot;wwDotnetBridgeDemos.dll&amp;quot;)

*!*	? &amp;quot;*** Raw Date to String&amp;quot;
*!*	*  public static string Humanize(this DateTime input, bool? utcDate = null, DateTime? dateToCompareAgainst = null, CultureInfo culture = null)      
*!*	ldDate = DATE() + 22
*!*	? loBridge.InvokeStaticMethod(&amp;quot;Humanizer.DateHumanizeExtensions&amp;quot;,&amp;quot;Humanize&amp;quot;,ldDate,null,null,null)  
?

*!*	? &amp;quot;*** Raw Number to String&amp;quot;
*!*	lnValue = 121233
*!*	* public static string ToWords(this int number, CultureInfo culture = null)       
*!*	? loBridge.InvokeStaticMethod(&amp;quot;Humanizer.NumberToWordsExtension&amp;quot;,&amp;quot;ToWords&amp;quot;,lnValue, null) + ;
*!*	&amp;quot;  (&amp;quot; + TRANSFORM(lnValue,&amp;quot;9,999,999&amp;quot;) + &amp;quot;)&amp;quot;
*!*	?

*** Using a .NET Wrapper Class

LOCAL loHuman as wwDotnetBridge.FoxHumanizer
loHuman = loBridge.CreateInstance(&amp;quot;wwDotnetBridgeDemos.FoxHumanizer&amp;quot;)

? &amp;quot;*** Human Friendly Dates&amp;quot;
? loHuman.HumanizeDate(DATE()-1)            &amp;amp;&amp;amp; yesterday
? loHuman.HumanizeDate(DATETime() + 86500)  &amp;amp;&amp;amp; tomorrow
? loHuman.HumanizeDate(DATE() + 2)          &amp;amp;&amp;amp; 2 days from now
? loHuman.HumanizeDate(DATETIME() - 55)     &amp;amp;&amp;amp; 55 seconds ago
? loHuman.HumanizeDate(DATETIME() - 3800)   &amp;amp;&amp;amp; an hour ago
?

? &amp;quot;*** Number to Words&amp;quot;
? loHuman.NumberToWords(10)        &amp;amp;&amp;amp; ten 
? loHuman.NumberToWords(1394)      &amp;amp;&amp;amp; one thousand three hundred ninety four
?
? &amp;quot;*** Pluralize&amp;quot;
? loHuman.Pluralize(&amp;quot;Building&amp;quot;)    &amp;amp;&amp;amp; Buildings
? loHUman.Pluralize(&amp;quot;Mouse&amp;quot;)       &amp;amp;&amp;amp; Mice
?

? &amp;quot;*** Numbers and Pluraize together&amp;quot;
? loHuman.ToQuantity(&amp;quot;Car&amp;quot;,3)       &amp;amp;&amp;amp; three cars
? loHuman.ToQuantity(&amp;quot;Mouse&amp;quot;,3)     &amp;amp;&amp;amp; three mice
?

? &amp;quot;*** Bytes, kb, megabytes etc. from bytes&amp;quot;
? loHuman.ToByteSize(13122)         &amp;amp;&amp;amp; 12.81 KB
? loHuman.ToByteSize(1221221)       &amp;amp;&amp;amp; 1.16 MB           
? loHuman.ToByteSize(1221221312)    &amp;amp;&amp;amp; 1.14 GB
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Humanizer has a ton of features and I've just exposed a few of them in the .NET class. Here's the C# class code:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;/// &amp;lt;summary&amp;gt;
 /// Helper to create humanized words of numbers dates and other occasions
 /// 
 /// Wrapper around hte Humanizer library:
 /// https://github.com/Humanizr/Humanizer
 /// &amp;lt;/summary&amp;gt;
 public class FoxHumanizer
 {

     /// &amp;lt;summary&amp;gt;
     /// Humanizes a date as yesterday, two days ago, a year ago, next month etc.
     /// &amp;lt;/summary&amp;gt;
     public string HumanizeDate(DateTime date)
     {            
         return date.Humanize(utcDate: false);
     }

     /// &amp;lt;summary&amp;gt;
     /// Turns integer numbers to words
     /// &amp;lt;/summary&amp;gt;
     public string NumberToWords(int number)
     {            
         return number.ToWords();
     }

     /// &amp;lt;summary&amp;gt;
     /// Returns a number like 1st, 2nd, 3rd
     /// &amp;lt;/summary&amp;gt;
     public string NumberToOrdinal(int number)
     {
         return number.Ordinalize();
     }

     public string NumberToOrdinalWords(int number)
     {
         return number.ToOrdinalWords();
     }

     /// &amp;lt;summary&amp;gt;
     /// creates expression like one car or two bananas
     /// from a qty and a string that is pluralized as needed
     /// &amp;lt;/summary&amp;gt;
     public string ToQuantity(string single, int qty)
     {
         return single.ToQuantity(qty, ShowQuantityAs.Words);
     }


     public string ToCamelCase(string input)
     {
         return input.Camelize();
     }

     /// &amp;lt;summary&amp;gt;
     /// Truncates a string and adds elipses after length is exceeded
     /// &amp;lt;/summary&amp;gt;
     public string TruncateString(string input, int length)
     {
         return input.Truncate(length);
     }

     /// &amp;lt;summary&amp;gt;
     /// Takes a singular noun and pluralizes it
     /// &amp;lt;/summary&amp;gt;
     public string Pluralize(string single)
     {
         return single.Pluralize(true);
     }

     /// &amp;lt;summary&amp;gt;
     /// Takes a pluralized noun and turns it to singular
     /// &amp;lt;/summary&amp;gt;
     public string Singularize(string pluralized)
     {
         return pluralized.Singularize(true);
     }

     /// &amp;lt;summary&amp;gt;
     /// Returns a byte count as kilobytes, Mb or Gb
     /// &amp;lt;/summary&amp;gt;
     public string ToByteSize(int byteSize)
     {
         return byteSize.Bytes().Humanize(&amp;quot;#.##&amp;quot;); 
     }
     
 }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Humanizer has a ton of features, so I picked specific features that I am interested in to expose in the FoxPro class. If there are other use cases that I have not figured out here I can add them separately.&lt;/p&gt;
&lt;p&gt;If I want to update the code I can simply update the code and rebuild the &lt;code&gt;wwDotnetBridgeDemos.csproj&lt;/code&gt; project which lives in its own folder in &lt;code&gt;.\Dotnet\wwDotnetBridgeDemos&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;To update the code and assuming I have the .NET SDK installed on my machine I can simply run the following from a terminal in that folder:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-cs"&gt;dotnet build wwDotnetBridgedemos.csproj
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The project is set up to automatically build into the &lt;code&gt;.\bin&lt;/code&gt; folder but make sure you've shut down FoxPro if the library was previously loaded.&lt;/p&gt;
&lt;h4 id="why-use-a-net-class"&gt;Why use a .NET Class?&lt;/h4&gt;
&lt;p&gt;So why did I use a .NET class if it's possible to call the code from FoxPro? In some cases it's simply easier to use .NET code to call functionality in .NET. Humanizer in particular uses a couple of features that are a little tricky to access from FoxPro.&lt;/p&gt;
&lt;p&gt;Consider this LinqPad code:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://west-wind.com/wconnect/weblog/imageContent/2025/White-Paper/Humanizer-LinqPad.png" alt="Humanizer LinqPad"&gt;&lt;/p&gt;
&lt;p&gt;Most of Humanizer's methods are heavily overloaded extension methods that use nullable types all of which are not super easy to call from FoxPro using wwDotnetBridge. It's possible, but it's super time consuming to find the right overload to call and then set up the ComValue structure correctly to support the nullable values.&lt;/p&gt;
&lt;p&gt;It's simply easier and also considerably more efficient to use a .NET wrapper directly for these calls especially since our own classes can be designed to be explicitly FoxPro friendly so they can be directly invoked without any of the wwDotnetBridge proxy helper methods.&lt;/p&gt;
&lt;h3 id="a-file-system-watcher-and-live-reload-event-handling"&gt;A File System Watcher and Live Reload (Event Handling)&lt;/h3&gt;
&lt;p&gt;&lt;small&gt;&lt;i&gt;&lt;/i&gt;&lt;/small&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Demonstrates:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Using built-in .NET functionality&lt;/li&gt;
&lt;li&gt;Handling events via a Callback class&lt;/li&gt;
&lt;li&gt;Useful for document change notifications&lt;/li&gt;
&lt;li&gt;Useful for a Live Reload Manager&lt;/li&gt;
&lt;li&gt;Uses &lt;code&gt;System.Net.FileSystemWatcher&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;The .NET &lt;code&gt;FileSystemWatcher&lt;/code&gt; is a very useful built-in class that can be used to monitor changes in the file system. You can enable the file watcher to notify you when files change, are added, deleted or renamed. There are many uses for this functionality especially in document centric applications.&lt;/p&gt;
&lt;h4 id="real-world-examples-of-monitoring-files"&gt;Real World Examples of Monitoring Files&lt;/h4&gt;
&lt;p&gt;For example, I use this functionality in Markdown Monster for example to detect when a document I'm editing has changed. If my document has no changes I can automatically update the document with the new changes from disk, or if there are changes I can pop up a notice when saving that the file has been changed since opening. At that point I can pop up a dialog letting me choose between my copy, the changed copy, or do text merge in a Comparison Tool (BeyondCompare for me). Here's what this looks like:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://github.com/RickStrahl/ImageDrop/blob/master/MarkdownMonster/FileChangeDetection.gif?raw=true" alt="Document File Change Detection"&gt;&lt;/p&gt;
&lt;p&gt;The way this works is that the File Watcher is created when the document is opened and I check for any changes for that specific file only. Then based on current document state the code executes an update or sets a flag that triggers the dialog on save.&lt;/p&gt;
&lt;p&gt;Another use case for this is in Web Connection for the Live Reloading the server if FoxPro code has changed. I can basically monitor for any code changes to PRG files, and if one has been change I can automatically restart the Web server application and run with the new change and at the same time trigger a page refresh in the browser to reload the page. Meanwhile the Web Connection Web connector which also uses a &lt;code&gt;FileSystemWatcher&lt;/code&gt; monitors for changes to Web files and automatically triggers a browser refresh when HTML, CSS or JS files change.&lt;/p&gt;
&lt;p&gt;It's a powerful feature and there are lots of use cases for it.&lt;/p&gt;
&lt;h4 id="using-async-callbacks-to-get-file-system-notifications-in-foxpro"&gt;Using Async Callbacks to get File System Notifications in FoxPro&lt;/h4&gt;
&lt;p&gt;The &lt;code&gt;FileSystemWatcher&lt;/code&gt; is a tricky component to work with, as it doesn't allow for filtering, so you basically monitor changes to &lt;strong&gt;all files&lt;/strong&gt; and then handle filtering as part of your event handling code. The component uses events which is something new to discuss in regards of FoxPro and wwDotnetBridge access to this component.&lt;/p&gt;
&lt;p&gt;Let's start with the setup code that starts the file monitoring:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;loBridge = GetwwDotnetBridge()

IF EMPTY(lcFileList)
   lcFileList = &amp;quot;*.prg,*.ini,*.fpw,*.bak,&amp;quot;
ENDIF
IF EMPTY(lcFolder)
   lcFolder = SYS(5) + CurDir()
ENDIF

*** Create an instance of the file watcher: MUST PERSIST!!!
PUBLIC __WWC_FILEWATCHER
__WWC_FILEWATCHER = loBridge.CreateInstance(&amp;quot;System.IO.FileSystemWatcher&amp;quot;,lcFolder)
__WWC_FILEWATCHER.EnableRaisingEvents = .T.
__WWC_FILEWATCHER.IncludeSubDirectories = .T.

*** Create the Handler Object that responds to events
loEventHandler = CREATEOBJECT(&amp;quot;FileWatcherEventHandler&amp;quot;)
loEventHandler.cFileList = lcFileList

*** Create a subscription to the events
loSubscription = loBridge.SubscribeToEvents(__WWC_FILEWATCHER, loEventHandler)
loEventHandler.oSubscription = loSubscription  &amp;amp;&amp;amp; so we can unsubscribe

? &amp;quot;Watching for file changes in: &amp;quot; + lcFolder + &amp;quot; for  &amp;quot; + lcFileList
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The code creates a new &lt;code&gt;FileSystemWatcher&lt;/code&gt; instance and enables listening to events. We specify a folder to monitor recursively and allow raising of events. The watcher exposes a number of events and we need to implement all events of the interface even if we are only interested in a few or even one event. This is similar to the way COM events are handled via FoxPro &lt;code&gt;INTERFACE&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Next we create an Event Subscriptiong by calling &lt;code&gt;SubscribeToEvents()&lt;/code&gt; to which we pass the object that we want to handle events on and an event handler object - that's the object that implements the methods matching the event signatures in .NET.&lt;/p&gt;
&lt;p&gt;The events are fired &lt;strong&gt;asynchronously&lt;/strong&gt;, meaning the event handler methods are fired out of band and the code immediately following the &lt;code&gt;SubscribeToEvents()&lt;/code&gt; immediately returns and keeps on running. The events fire in the background, out of band.&lt;/p&gt;
&lt;p&gt;The event handler class is then called when something interesting happens - in this case when files are changed in some way. For this example, I'm simply writing the operation and filename to the desktop, but you can do anything you want from within the event code.&lt;/p&gt;
&lt;p&gt;You do however, want to &lt;strong&gt;minimize the code you run in the event handler&lt;/strong&gt; and have it touch as little shared data as possible. It's best to consider event handlers as data drops where you dump some data that can be picked up by the main application at a later time. This can be as simple as setting one or more variables that are later read, or writing a record to a database table that is later read.&lt;/p&gt;
&lt;p&gt;Here's the EventHandler class:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;*************************************************************
DEFINE CLASS FileWatcherEventHandler AS Custom
*************************************************************

*** File extensions to monitor
cFileList = &amp;quot;*.prg,*.vcx,*.exe,*.app,*.ini&amp;quot;

*** Optional comma list of folders to exclude (ie: \temp\,\web\)
cFolderExclusions = &amp;quot;&amp;quot;

*** File watcher subscription
oSubscription = null

nLastAccess = 0

************************************************************************
FUNCTION HasFileChanged(lcFilename as String)
****************************************
LOCAL lcFileList, lnX, lnFiles, lcExtension, lnExclusions
LOCAL ARRAY laExtensions[1]
LOCAL ARRAY laExclusions[1]

IF EMPTY(lcFileName)
   RETURN .F.
ENDIf


IF ATC(&amp;quot;\temp\&amp;quot;,lcFileName) &amp;gt; 0
   RETURN .F.
ENDIF

lnExclusions = 0
IF !EMPTY(THIS.cFolderExclusions)
	lnExclusions = ALINES(laExclusions,this.cFolderExclusions,1 + 4, &amp;quot;,&amp;quot;)
	FOR lnX = 1 TO lnExclusions
	    IF ATC(LOWER(laExclusions[lnX]),lcFileName) &amp;gt; 0
	       RETURN .F.
	    ENDIF
	ENDFOR
ENDIF

lcFileList = STRTRAN(THIS.cFileList,&amp;quot;*.&amp;quot;,&amp;quot;&amp;quot;)
lnFiles =  ALINES(laExtensions,lcFileList,1 + 4,&amp;quot;,&amp;quot;)
        
FOR lnX = 1 TO lnFiles
    lcExtension = LOWER(JUSTEXT(lcFileName))
    IF lcExtension == LOWER(laExtensions[lnX])
       this.nLastAccess = SECONDS()
       RETURN .T.
    ENDIF
ENDFOR

RETURN .F.
ENDFUNC
*   HasFileChanged


************************************************************************
FUNCTION OnCreated(sender,ev)
****************************************
LOCAL lcFile 

lcFile = THIS.GetFilename(ev)
IF THIS.HasFileChanged(lcFile)
   ? &amp;quot;File has been created: &amp;quot; +lcFile
ENDIF
	
ENDFUNC

FUNCTION OnChanged(sender,ev)
LOCAL lcFile 

lcFile = THIS.GetFilename(ev)

IF THIS.HasFileChanged(lcFile)
	? &amp;quot;File has been changed: &amp;quot; + lcFile
ENDIF

ENDFUNC

************************************************************************
FUNCTION OnDeleted(sender, ev)
******************************
LOCAL lcFile 

lcFile = THIS.GetFilename(ev)
IF THIS.HasFileChanged(lcFile)
	? &amp;quot;File has been deleted: &amp;quot; + lcFile
ENDIF

ENDFUNC

************************************************************************
FUNCTION OnRenamed(sender, ev)
******************************
LOCAL lcFile 

IF VARTYPE(ev) # &amp;quot;O&amp;quot;
   RETURN
ENDIF

*** RenamedEventArgs apparently doesn't allow direct access
loBridge = GetwwDotnetBridge()
lcOldFile = loBridge.GetProperty(ev,&amp;quot;OldFullPath&amp;quot;)
IF EMPTY(lcOldFile)
   RETURN
ENDIF
lcNewFile = loBridge.GetProperty(ev,&amp;quot;FullPath&amp;quot;)
IF EMPTY(lcNewFile)
   RETURN
ENDIF

? &amp;quot;File has been renamed: &amp;quot; + lcOldFile + &amp;quot; to &amp;quot; + lcNewFile

ENDFUNC

************************************************************************
FUNCTION Destroy()
******************

IF THIS.oSubscription != null
	THIS.oSubscription.UnSubscribe()
	THIS.oSubscription = null
ENDIF
   
IF VARTYPE(__WWC_FILEWATCHER) = &amp;quot;O&amp;quot;
   __WWC_FILEWATCHER.Dispose()
   __WWC_FILEWATCHER = .F.
ENDIF

ENDFUNC

ENDDEFINE
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The file watcher has no file matching filters that can be applied to the events which means that you have to filter for files that you want to watch as part of the event handlers you implement. If you're only interested in PRG files you need to check what files are incoming and immediately exit if it's not what you want.&lt;/p&gt;
&lt;p&gt;Another tricky part about the file watcher is knowing exactly what the event interface looks like which &lt;a href="https://learn.microsoft.com/en-us/dotnet/api/system.io.filesystemwatcher.changed?view=net-8.0"&gt;in this case can be found here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;As mentioned &lt;strong&gt;all events on the event interface have to be implemented&lt;/strong&gt;, but you can certainly provide do nothing event handling the simple exit immediately. If you don't care about renaming, just immediately return in the &lt;code&gt;OnRenamed&lt;/code&gt; event.&lt;/p&gt;
&lt;p&gt;When you run this demo you might also notice that some events like the change events fire multiple times. That's because there are filters that you can set on what can be monitored and many file operation might trigger for multiple matches to these triggers. It might be file data change and data change. Making the file watcher behave often involves ignoring repeated events and playing around with the event filters.&lt;/p&gt;
&lt;p&gt;Regardless of the minor issues, a Filewatcher is a powerful tool that's useful for a ton of features.&lt;/p&gt;
&lt;h3 id="async-print-html-to-pdf"&gt;Async: Print Html to Pdf&lt;/h3&gt;
&lt;p&gt;&lt;small&gt;&lt;i&gt;&lt;/i&gt;&lt;/small&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Demonstrates:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Calling async Task methods&lt;/li&gt;
&lt;li&gt;Using Callback handlers to handle async completions&lt;/li&gt;
&lt;li&gt;Find out how to turn HTML to PDF&lt;/li&gt;
&lt;li&gt;NuGet Library: &lt;a href="https://github.com/RickStrahl/Westwind.WebView.HtmlToPdf"&gt;Westwind.WebView.HtmlToPdf&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;This is a small example that demonstrates how to print HTML documents to PDF files. This is useful for Web and desktop applications that can output reports and documents to HTML and turn that HTML into PDF files that can be emailed or published.&lt;/p&gt;
&lt;p&gt;This and the following example require the use of &lt;code&gt;async Task&lt;/code&gt; code in .NET as the task of printing to PDF can take a bit of time. So rather than waiting for completion the operation is &lt;code&gt;async&lt;/code&gt; and calls you back when the task is complete.&lt;/p&gt;
&lt;h4 id="introducing-invoketaskmethodasync"&gt;Introducing InvokeTaskMethodAsync()&lt;/h4&gt;
&lt;p&gt;wwDotnetBridge includes a &lt;code&gt;InvokeTaskMethodAsync()&lt;/code&gt; which calls an async method and responds back via a Callback object that you pass in. This is similar to the way the FoxPro &lt;code&gt;EVENTHANDLER()&lt;/code&gt; function works except that we're passing the callback and event method through to .NET rather than to a COM object.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;AsyncCallbackEvents&lt;/code&gt; handler object that is passed to the method is notified when the async operation completes either via a &lt;code&gt;OnComplete()&lt;/code&gt; or &lt;code&gt;OnError()&lt;/code&gt; handler. Unlike the event interface we discussed in the last example, here we only have to implement two methods and they always have the same signature, although &lt;code&gt;OnComplete()&lt;/code&gt; will receive a different result each time, depending on a successful return value from the async operation.&lt;/p&gt;
&lt;h4 id="printing-pdf-output-from-html-input"&gt;Printing PDF Output from HTML Input&lt;/h4&gt;
&lt;p&gt;Let's take a look by way of an example that uses a third party (mine) .NET component that converts PDF documents to HTML using the Edge WebView runtime in Windows using the &lt;code&gt;Westwind.WebView.HtmlToPdf&lt;/code&gt; component.&lt;/p&gt;
&lt;p&gt;Let's start with the mainline setup code that starts the Html to Pdf operation.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;LPARAMETER lcUrl, lcOutputFile

*** Has to be persist after program ends 
PUBLIC loCallbacks

IF EMPTY(lcUrl)
   lcUrl = &amp;quot;Assets/HtmlSampleFile-SelfContained.html&amp;quot;
ENDIF
IF EMPTY(lcOutputFile)
   lcOutputFile = &amp;quot;c:\temp\htmltopdf.pdf&amp;quot;
ENDIF   
IF !StartsWith(lcUrl, &amp;quot;http&amp;quot;)  
   *** Assume it's a path - fix it up
   lcUrl = GetFullPath(lcUrl)
ENDIF

CLEAR 
? &amp;quot;*** Generating PDF from &amp;quot; + lcUrl
? &amp;quot;*** To &amp;quot; + lcOutputFile

LOCAL loBridge as wwDotNetBridge
loBridge = GetwwDotnetBridge()
loBridge.LoadAssembly(&amp;quot;Westwind.WebView.HtmlToPdf.dll&amp;quot;)

*** Create the .NET class
loPdf = loBridge.CreateInstance(&amp;quot;Westwind.WebView.HtmlToPdf.HtmlToPdfHost&amp;quot;)

*** Create the Callback Handler Object
loCallbacks = CREATEOBJECT(&amp;quot;PdfCallbacks&amp;quot;)
loCallbacks.cOutputFile = lcOutputFile
ERASE (lcOutputFile)

loSettings = null
loSettings = loBridge.CreateInstance(&amp;quot;Westwind.WebView.HtmlToPdf.WebViewPrintSettings&amp;quot;)

*** Async PDF Generation Method: Exits immediately
loBridge.InvokeTaskmethodAsync(loCallbacks, loPdf,
                               &amp;quot;PrintToPdfAsync&amp;quot;, lcUrl, 
                               lcOutputFile, loSettings)

? &amp;quot;*** Converting to PDF - this may take a few seconds...&amp;quot;
GoUrl(lcUrl)   &amp;amp;&amp;amp; Display PDF
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The base behavior for this class is very simple - you call a single method to perform the Html to PDF conversion. You basically provide a Url or Filename to an HTML document, and an optional output filename. The engine then renders the HTML in the WebView instance and uses the WebView's built-in PDF engine - the same engine that's used for printing and PDF printing - and renders the active HTML document as best as it can to PDF.&lt;/p&gt;
&lt;blockquote&gt;
&lt;h5 id="--pdf-output-generation-isnt-an-exact-science"&gt;&lt;i class="fas fa-info-circle" style="font-size: 1.1em"&gt;&lt;/i&gt;  PDF Output Generation isn't an exact Science&lt;/h5&gt;
&lt;p&gt;PDF output is basically &lt;strong&gt;Print output&lt;/strong&gt;, so the PDF output generated will be the same as what you see in Print Preview window of any Chromium browser. So if you want to get an idea how well PDF printing works for a specific document/url you can test the output before you ever try to print it.&lt;/p&gt;
&lt;p&gt;Print output of HTML can be effected by HTML styling - not everything that renders well as HTML prints well. Additionally it's very useful to add print specific CSS styling that simplifies HTML rendering, uses specific built-in fonts etc. to optimize printing.  When it comes to print output simpler is better - and plain semantic HTML (ie. document centric output) works best.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The call &lt;code&gt;PrintToPdfAsync()&lt;/code&gt; is async so it returns immediately. You pass in a Callback handler object that is called back when the print operation completes - or fails. You also pass the Url, output file and a settings object which allows customizing a few things about the PDF output generation.&lt;/p&gt;
&lt;p&gt;Here's what the Callback object looks like:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;DEFINE CLASS PdfCallbacks as AsyncCallbackEvents

*** Capture output file 
cOutputFile = &amp;quot;&amp;quot;

FUNCTION OnCompleted(lvResult,lcMethod)

IF !lvResult.IsSuccess
   ? &amp;quot;ERROR: &amp;quot; + lvResult.Message
   RETURN
ENDIF

GoUrl(THIS.cOutputFile)
? &amp;quot;*** PDF Output Generated!&amp;quot;

ENDFUNC

FUNCTION OnError(lcMessage, loException, lcMethod)

? &amp;quot;Error: &amp;quot; + lcMethod,lcMessage

ENDFUNC

ENDDEFINE
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You inherit from AsyncCallbackEvents and implement two methods:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;OnCompleted(lvResult, lcMethod)&lt;/li&gt;
&lt;li&gt;OnError(lcMessage, loException, lcMethod)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The reason for inheriting is that in some cases you may not need to handle either of these methods - in fire and forget scenarios for examples. In other cases you don't care about errors and you can omit the &lt;code&gt;OnError&lt;/code&gt; handler - the base class can capture and ignore the events. &lt;strong&gt;One of these two events will always be called&lt;/strong&gt; though - and hence the base class to ensure that the methods exist.&lt;/p&gt;
&lt;p&gt;In this example, on success we simply go and display the PDF file in the browser via &lt;code&gt;GoUrl(this.cOutputFile)&lt;/code&gt;. Note that the class adds a custom &lt;code&gt;cOutputFile&lt;/code&gt; property that is set when the class is instantiated. Use the property interface to pass values that you need from the mainline into the callback class.&lt;/p&gt;
&lt;p&gt;As with the Event code in the previous example, keep event handler methods very short running and have minimal impact on the application state. Ideally capture what you need store it somewhere and then process it from the mainline code.&lt;/p&gt;
&lt;p&gt;Here's what the output from the parameterless sample looks like from a local self-contained long HTML document printed to PDF:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://west-wind.com/wconnect/weblog/imageContent/2025/White-Paper/PrintToPdfOutput.png" alt="Print To Pdf Output"&gt;&lt;/p&gt;
&lt;p&gt;The demo displays both the original HTML and converted PDF document. Here's another example by running to a specific Url:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;DO PrintToPdf with &amp;quot;https://microsoft.com&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here's what the Microsoft Site output looks like (they do a pretty good job with their Print stylesheet):&lt;/p&gt;
&lt;p&gt;&lt;img src="https://west-wind.com/wconnect/weblog/imageContent/2025/White-Paper/PrintToPdfOutputMicrosoftCom.png" alt="Print To Pdf Output Microsoft Com"&gt;&lt;/p&gt;
&lt;p&gt;Note that the print results may very in quality, depending how well the URL/HTML support print output via a print specific stylesheet. The library adds some basic print styling but PDF rendering may not do a great job on really complex, and non-semantic HTML documents. It does however very well with very document centric output like documentation or output from - Markdown documents.&lt;/p&gt;
&lt;h3 id="async-openai-calls"&gt;Async: OpenAI Calls&lt;/h3&gt;
&lt;p&gt;&lt;small&gt;&lt;i&gt;&lt;/i&gt;&lt;/small&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Demonstrates:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Calling async Task methods&lt;/li&gt;
&lt;li&gt;Using Callback handlers to handle async completions&lt;/li&gt;
&lt;li&gt;Use an OpenAI API library to access various OpenAI services online and locally&lt;/li&gt;
&lt;li&gt;Some useful scenarios for AI&lt;/li&gt;
&lt;li&gt;NuGet Library: &lt;a href="https://github.com/RickStrahl/Westwind.Ai"&gt;Westwind.Ai&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;For this example I'll create several different types of AI interfaces that perform specific tasks:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Text Summarization&lt;/li&gt;
&lt;li&gt;Translations&lt;/li&gt;
&lt;li&gt;Generic Chat Completions&lt;/li&gt;
&lt;li&gt;Image Generation&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;OpenAI is a company, but the company was one of the first to expose AI as a service, using the OpenAI HTTP API. This API has become an unofficial open standard and so you find OpenAI style APIs that work with multiple AI engines. In this example, we'll use the HTTP service via front end library that abstracts and wraps the API communications and handles the async calls to the server.&lt;/p&gt;
&lt;h4 id="westwindai-library"&gt;Westwind.AI Library&lt;/h4&gt;
&lt;p&gt;To do this I'll use one of my own .NET Libraries called &lt;a href="https://github.com/RickStrahl/Westwind.Ai"&gt;Westwind.AI&lt;/a&gt; that can interface via HTTP with any OpenAI style API. Using this library you can easily connect to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;OpenAI&lt;/li&gt;
&lt;li&gt;Azure OpenAI&lt;/li&gt;
&lt;li&gt;Local Ollama Models&lt;/li&gt;
&lt;li&gt;Any generic OpenAI server&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The library is set up in a way that you can easily switch between service providers so you can quickly switch between online models and local models.&lt;/p&gt;
&lt;p&gt;The library supports both Chat Completion interfaces as well as image generation via the OpenAI Dall-E 3 model.&lt;/p&gt;
&lt;h4 id="online-or-local-ais-llms-and-slms---oh-my"&gt;Online or Local AIs? LLMs and SLMs - Oh my!&lt;/h4&gt;
&lt;p&gt;AI comes in all sorts of sizes, but most of you are probably familiar with the large public models or LLMs (Large Language Models) like ChatGPT or CoPilot (Microsoft), Gemini (Google), Llama (Meta) and Grok (X). These are commercial Chat bots that typically run in the browser and communicate with online APIs to provide AI results.&lt;/p&gt;
&lt;p&gt;ChatGPT and CoPilot both use OpenAI's models, and they are at the moment typically using GTP-4 or GPT-4o-mini to serve chat requests.&lt;/p&gt;
&lt;p&gt;Web based chat bots are cool to play with and useful for assistance tasks, but you can also access these very same models directly from your applications, by using the OpenAI API that is exposed by OpenAI, and Microsoft's Azure services online, or for local machines and many supported local models using Ollama (there are others that also use OpenAI APIs but I haven't tried them since Ollama works well). In other words you can use the APIs to access the power of both online LLMs and offline SLMs (Small Language Models) that can run offline on your own machine.&lt;/p&gt;
&lt;p&gt;At the moment online models tend to be much better at providing results, and also much faster than local models. Running local models on non-AI optimized hardware is pretty slow even when running on a reasonably poweful GPU. While many results for say summarizing a 5000 word document via an LLM takes maybe 5 seconds, it can take 30 seconds with a local SLM. The results also tend to be much more... variable with local SLMs. Most of the SLMs I've used for the examples I show here, produce very inconsistent results while the online LLM provide pretty solid results. I suspect this will improve in the future as LLMs are quickly becoming unsustainable for energy and resource usage, and more AI processing eventually will have to local machines. But we're not there yet...&lt;/p&gt;
&lt;p&gt;So for best results you'll likely want to use one of the major online LLMs and this library specifically supports OpenAI and Azure OpenAI. Both of these run OpenAI models and if you're using one of these you'll likely use either &lt;code&gt;gpt-4o-mini&lt;/code&gt; or &lt;code&gt;gpt-4o&lt;/code&gt;. The mini model is smaller, faster and much cheaper version of the full blown &lt;code&gt;gpt-4&lt;/code&gt; models.&lt;/p&gt;
&lt;p&gt;Both of these online models are paid services and you pay for API access. OpenAI has a pretty simple credit card signup process and you are charged in relatively small increments. Pricing is quite reasonable even though I use the Image Generation and AI features in Markdown Monster extensively I only replenish my account's $20 balance limit once every two months.  Azure Open AI has more complex billing tied to Azure and it uses the same models that OpenAI publishes (with some delay in latest model availability). But it's much more complex to host models as you in effect host your own model service that is tied to a specific model type. Using Azure only makes sense if you have free credits because of a subscription or benefit of some kind, or if you are already heavily using Azure.&lt;/p&gt;
&lt;h4 id="running-local-ai-models"&gt;Running Local AI Models&lt;/h4&gt;
&lt;p&gt;Although I mentioned that local models tend to be slower and generally less accurate, it's still very cool that you can run models locally. One huge benefit of running local models in a tool like &lt;a href="https://ollama.com"&gt;Ollama&lt;/a&gt; is that &lt;strong&gt;you are not sending your inputs to an online provider so you have privacy&lt;/strong&gt;. Also the offline models tend to have much more lax rules of what is allowed in AI output generation. With online models it's very easy to run into &lt;strong&gt;safety restrictions&lt;/strong&gt; when running chat or image queries while local models tend to have restrictions dialed down by default. There are definite free speech restrictions that come into play often in unexpected ways especially as chat and image queries are fixed up by the AI engines. It's much more pronounced with the online services, less so with local AIs although it all depends on how the AI model was tuned.&lt;/p&gt;
&lt;p&gt;IAC, for local models I recommend using &lt;a href="https://ollama.com/"&gt;Ollama&lt;/a&gt; which is a locally installable AI Host server. You can download and install it on Windows, and then use command line commands to pull down and run models locally. By default there's a command line interface that you can use, but a better way is to install &lt;a href="https://github.com/open-webui/open-webui"&gt;Open WebUI&lt;/a&gt; which lets you run a local browser Chat interface to Ollama. This is similar to ChatGPT or CoPilot, but using local models. Open WebUI also lets you find and download models, and easily switch between multiple installed local models.&lt;/p&gt;
&lt;h4 id="generic-chat-completions-from-foxpro"&gt;Generic Chat Completions from FoxPro&lt;/h4&gt;
&lt;p&gt;Ok, let's take a look at a few practical examples using &lt;strong&gt;Chat Completions&lt;/strong&gt;. Chat completions is basically the same interface that you use with ChatGPT or CoPilot where &lt;strong&gt;you specify a prompt&lt;/strong&gt; and the AI provides a response. You can provide an &lt;strong&gt;Input Prompt&lt;/strong&gt; which is your request, and also a &lt;strong&gt;System Prompt&lt;/strong&gt; which assigns the AI a &lt;strong&gt;Role&lt;/strong&gt;. The default role of most general purpose AIs is &lt;em&gt;You are a helpful assistant.&lt;/em&gt;, but you can modify that.&lt;/p&gt;
&lt;p&gt;So I start with two practical examples that provide Text Summaries and Language Translation.&lt;/p&gt;
&lt;h6 id="text-summary"&gt;Text Summary&lt;/h6&gt;
&lt;p&gt;So the first example is using chat completions to do text summaries of a block of text. This is a quite useful feature and using the library I use here, relatively easy to do.&lt;/p&gt;
&lt;p&gt;Let's start with LinqPad to see what the .NET code looks like:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://west-wind.com/wconnect/weblog/imageContent/2025/White-Paper/OpenAi-Summary-LinqPad.png" alt="OpenAi Summary LinqPad"&gt;&lt;/p&gt;
&lt;p&gt;As you can see the key bit of code is the &lt;code&gt;async&lt;/code&gt; call:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-cs"&gt;string result = await chat.Complete(prompt, systemPrompt, false);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Like in the Html to Pdf example we'll need to using &lt;code&gt;InvokeTaskMethodAsync()&lt;/code&gt; with a Callback object in FoxPro in order to get asynchronously called back when the relatively slow chat completion returns:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;LPARAMETERS lcTextToSummarize
LCOAL loBridge as wwDotNetBridge, loCompletions

loBridge = GetwwDotnetBridge()

*** Using OpenAI API
loConnection = loBridge.CreateInstance(&amp;quot;Westwind.AI.Configuration.OpenAiConnection&amp;quot;)
loConnection.ApiKey = GETENV(&amp;quot;OPENAI_KEY&amp;quot;)
loConnection.ModelId = &amp;quot;gpt-4o-mini&amp;quot;  &amp;amp;&amp;amp; &amp;quot;gpt-3-turbo&amp;quot; (default so not really neccessary)

IF EMPTY(lcTextToSummarize)
    ? &amp;quot;Please provide some text to summarize.&amp;quot;
	RETURN
ENDIF

*** Create Chat client and pass configuration
loCompletions = loBridge.CreateInstance(&amp;quot;Westwind.AI.Chat.GenericAiChatClient&amp;quot;, loConnection)

*** Our prompt text/question
lcPrompt = lcTextToSummarize

*** Specify a role for the AI
lcSystem = &amp;quot;You are a technical writer and are tasked to summarize input text succinctly &amp;quot; +;
           &amp;quot;in no more than 4 sentences. Return only the result summary text.&amp;quot;

*** Callback object that fires OnComplete/OnError events translation is done
loCallback = CREATEOBJECT(&amp;quot;OpenAiCallback&amp;quot;)
loCallback.oCompletions = loCompletions  

*** Async call starts here - returns immediately
loBridge.InvokeTaskMethodAsync(loCallback, loCompletions,&amp;quot;Complete&amp;quot;,lcPrompt, lcSystem, .F.)

? &amp;quot;*** Program completes. Async call continues in background.&amp;quot;
? &amp;quot;Summarizing...&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This code is made up of three steps:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Setting up the OpenAI Connection&lt;/li&gt;
&lt;li&gt;Setting up the prompt&lt;/li&gt;
&lt;li&gt;Calling the OpenAI API&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The code's comments describe each operation, as it occurs. The async call returns immediately and there's no success or failure information. Instead the result is handled in the &lt;code&gt;OpenAiCallback&lt;/code&gt; class which implements the &lt;code&gt;OnCompleted()&lt;/code&gt;  and &lt;code&gt;OnError()&lt;/code&gt; methods:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;DEFINE CLASS OpenAICallback as AsyncCallbackEvents

oCompletions = null

*** Returns the result of the method and the name of the method name
FUNCTION OnCompleted(lcResult,lcMethod)

IF (this.oCompletions.HasError)
    ? &amp;quot;Error: &amp;quot; + THIS.oCompletions.ErrorMessage
    RETURN
ENDIF

? &amp;quot;Summary:&amp;quot;
? &amp;quot;----------&amp;quot;
? lcResult

ENDFUNC

* Returns an error message, a .NET Exception and the method name
FUNCTION OnError(lcMessage,loException,lcMethod)
? &amp;quot;Error: &amp;quot; + lcMethod,lcMessage
ENDFUNC

ENDDEFINE
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To test this out you can copy some text to the clipboard and let it summarize. Just for kicks I took the massive Markdown content of this very white paper and put it in my clipboard. On my machine the result comes back in about 3-4 seconds with a very capable response.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;OnCompleted()&lt;/code&gt; method receives the result value from the async call, along with the name of the method that was called. The latter can be useful if you use the same Callback handler for multiple methods or calls simultaneously and these results might come back at different times.&lt;/p&gt;
&lt;p&gt;The value in this case is a text completion, so it's plain text.&lt;/p&gt;
&lt;p&gt;OpenAI responses can be returned as text, which in many cases can be markdown. For direct scenarios like the above of text summarization the text is usually returned as plain text as there's nothing to really format. However, for more complex results you will often see Markdown. We'll look at some examples later that demonstrate how to deal with that.&lt;/p&gt;
&lt;h4 id="translations"&gt;Translations&lt;/h4&gt;
&lt;p&gt;For the next example lets do essentially the same thing, except this time we'll run translations from one language to another. Essentially the code here is nearly identical to the last example, except that the prompt and system prompt are different along with some of the messages:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;LPARAMETERS lcTranslateText, lcLanguage

*** We have to keep the completions alive
loBridge = GetwwDotnetBridge()
loBridge.LoadAssembly(&amp;quot;Westwind.Ai.dll&amp;quot;)

lcOpenAiKey = GETENV(&amp;quot;OPENAI_KEY&amp;quot;)

loConnection = loBridge.CreateInstance(&amp;quot;Westwind.AI.Configuration.OpenAiConnection&amp;quot;)

loConnection.ApiKey = lcOpenAiKey
loConnection.ModelId = &amp;quot;gpt-4o-mini&amp;quot;

IF EMPTY(lcTranslateText)
   lcTranslateText = &amp;quot;Genius is one percent inspiration, ninety-nine percent perspiration.&amp;quot;
ENDIF
IF EMPTY(lcLanguage)
  lcLanguage = &amp;quot;German&amp;quot;
ENDIF  

loCompletions = loBridge.CreateInstance(&amp;quot;Westwind.AI.Chat.GenericAiChatClient&amp;quot;, loConnection)
lcSystem = &amp;quot;You are a translator and you translate text from one language to another. &amp;quot; +;
           &amp;quot;Return only the translated text.&amp;quot;
lcPrompt = &amp;quot;Translate from English to &amp;quot; + lcLanguage + CHR(13) + CHR(13) + lcTranslateText

*** Set up the callback event handler - OnCompleted/OnError
loCallback = CREATEOBJECT(&amp;quot;OpenAiCallback&amp;quot;)
loCallback.oCompletions = loCompletions &amp;amp;&amp;amp; pass so we can access in callback

*** Make the API call asynchronously - returns immediately
loBridge.InvokeTaskMethodAsync(loCallback, loCompletions,&amp;quot;Complete&amp;quot;,lcPrompt, lcSystem, .F.)

? &amp;quot;Translating from English...&amp;quot;
? &amp;quot;--------------&amp;quot;
? lcTranslateText

****************************************************************
DEFINE CLASS OpenAICallback as AsyncCallbackEvents
**************************************************
oCompletions = null

*** Returns the result of the method and the name of the method name
FUNCTION OnCompleted(lcResult,lcMethod)

IF (this.oCompletions.HasError)
    ? &amp;quot;Error: &amp;quot; + this.oCompletions.ErrorMessage
    RETURN
ENDIF

? &amp;quot;To German:&amp;quot;
? &amp;quot;----------&amp;quot;
? lcResult
ENDFUNC

FUNCTION OnError(lcMessage,loException,lcMethod)
? &amp;quot;Error: &amp;quot; + lcMethod,lcMessage
ENDFUNC

ENDDEFINE
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Again this sample works best with text from the clipboard. Select and copy some text, then translate by doing:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;DO openAITranslation with _Cliptext, &amp;quot;German&amp;quot;


DO openAITranslation with _Cliptext, &amp;quot;French&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Cool, right?&lt;/p&gt;
&lt;p&gt;Again, if you run these operations multiple times, you'll find that each time it'll translate slightly differently using different words or phrasings. OpenAI does a pretty good job however - I speak fluent German and it usually returns even colloquial German text, even though there's variance.&lt;/p&gt;
&lt;h4 id="using-local-llms-with-ollama"&gt;Using Local LLMs with Ollama&lt;/h4&gt;
&lt;p&gt;Using the OpenAI online model uses the OpenAIConnection like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;*** Using OpenAI API
loConnection = loBridge.CreateInstance(&amp;quot;Westwind.AI.Configuration.OpenAiConnection&amp;quot;)
loConnection.ApiKey = GETENV(&amp;quot;OPENAI_KEY&amp;quot;)
loConnection.ModelId = &amp;quot;gpt-4o-mini&amp;quot;  &amp;amp;&amp;amp; &amp;quot;gpt-3-turbo&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For OpenAI you specify your API key and optionally a model id. In this case the model ID isn't really necessary because currently &lt;code&gt;gpt-4o-mini&lt;/code&gt; is the default model.&lt;/p&gt;
&lt;p&gt;If I want to run this same example with a local SLM model via Ollama I can do that as well - all I have to do is change the connection. Assuming Ollama is running on my machine, I can change the &lt;code&gt;Connection&lt;/code&gt; configuration to:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;*** Using Ollama SMLs Locally
loConnection = loBridge.CreateInstance(&amp;quot;Westwind.AI.Configuration.OllamaOpenAiConnection&amp;quot;)
loConnection.ModelId = &amp;quot;llama3&amp;quot;    &amp;amp;&amp;amp; specify a local model (llama3, phi3.5, mistral etc.)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Local Ollama doesn't require an API key so the only thing you have to provide is the local model you want to run. Even that is optional as the default active model is used.&lt;/p&gt;
&lt;p&gt;Running against a local model you'll quickly find out that requests are significantly slower and that results are much more variable.&lt;/p&gt;
&lt;p&gt;For example, &lt;code&gt;llama3&lt;/code&gt; (Meta's model that has an SLM version) often ignores my request to only return the actual results. For summaries it often refuses to stick to the 4 paragraph limit and gives me a freaking novel instead. &lt;code&gt;phi3.5&lt;/code&gt; (Microsoft's SLM model) often produces bad translations that miss words.&lt;/p&gt;
&lt;p&gt;The OpenAI LLMs do a much better job at returning results quickly, and producing more consistently accurate results.&lt;/p&gt;
&lt;h4 id="generic-chat-completions"&gt;Generic Chat Completions&lt;/h4&gt;
&lt;p&gt;The last two examples showed how to create specific implementation of chat completions with pre-defined prompts and tasks: Text summarization and translation specifically.&lt;/p&gt;
&lt;p&gt;But sometimes it's also useful to just have a generic AI that can provide completely random information. And that's easy to do by simply changing around the way the prompt and system prompt are handled.&lt;/p&gt;
&lt;p&gt;Same idea as the last examples, except in this version we can pass in the prompt directly along with an optional system prompt.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;LPARAMETERS lcPrompt, lcSystemPrompt

do wwDotNetBridge
DO markdownParser

LOCAL loBridge as wwDotNetBridge, loCompletions
loBridge = GetwwDotnetBridge()
loBridge.LoadAssembly(&amp;quot;Westwind.Ai.dll&amp;quot;)

lcOpenAiKey = GETENV(&amp;quot;OPENAI_KEY&amp;quot;)

*** Open AI Connection with gpt-4o-mini
loConnection = loBridge.CreateInstance(&amp;quot;Westwind.AI.Configuration.OpenAiConnection&amp;quot;)
loConnection.ApiKey = lcOpenAiKey

IF EMPTY(lcPrompt)
   lcPrompt = &amp;quot;How do I make an Http call in FoxPro with wwHttp?&amp;quot;
ENDIF
IF EMPTY(lcSystemPrompt)
  lcSystemPrompt = &amp;quot;You are a general purpose, helpful assistant&amp;quot;
ENDIF  

poCompletions = loBridge.CreateInstance(&amp;quot;Westwind.AI.Chat.GenericAiChatClient&amp;quot;, loConnection)

loCallback = CREATEOBJECT(&amp;quot;OpenAiCallback&amp;quot;)
loCollback.oCompletions = loCompletions
loBridge.InvokeTaskMethodAsync(loCallback, loCompletions,&amp;quot;Complete&amp;quot;,lcPrompt, lcSystemPrompt, .F.)

? &amp;quot;Thinking...&amp;quot;
? &amp;quot;--------------&amp;quot;
? lcPrompt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nothing new here, except I'm passing in the &lt;code&gt;lcPrompt&lt;/code&gt; and optional &lt;code&gt;lcSystemPrompt&lt;/code&gt; so we can do:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;DO openAICompletions with &amp;quot;Tell me about the history of Southwest Fox&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This time the result will likely be a little different with more information and the interfaces tend to return text as &lt;strong&gt;Markdown&lt;/strong&gt;. Notice at the top I &lt;code&gt;DO MarkdownParser&lt;/code&gt; which was covered in one of the earlier samples. We'll use the Markdown parser to parse the result to HTML and then display the result in a templated HTML template so it looks nice.&lt;/p&gt;
&lt;p&gt;As before the results are handled in the Async Callback via the &lt;code&gt;OpenAiCallback&lt;/code&gt; class:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;DEFINE CLASS OpenAICallback as AsyncCallbackEvents

oCompletions = null

*** Returns the result of the method and the name of the method name
FUNCTION OnCompleted(lcResult,lcMethod)

IF (This.oCompletions.HasError)
    ? &amp;quot;Error: &amp;quot; + THIS.oCompletions.ErrorMessage
    RETURN
ENDIF

*** Convert to Markdown (MarkdownParser.prg)
lcHtml = Markdown(lcResult)

? &amp;quot;Done!&amp;quot;

*** Show Web Page with Formatting
ShowWebPage(lcHtml)

ENDFUNC

* Returns an error message, a .NET Exception and the method name
FUNCTION OnError(lcMessage,loException,lcMethod)
? &amp;quot;Error: &amp;quot; + lcMethod,lcMessage
ENDFUNC

ENDDEFINE
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here's what the result looks like with the result returned in a couple of seconds:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://west-wind.com/wconnect/weblog/imageContent/2025/White-Paper/OpenAi-Completions-SwFoxHistory.png" alt="OpenAi Completions SwFoxHistory"&gt;&lt;/p&gt;
&lt;p&gt;While it's cool to see this work and come back with a useful result, think hard about whether you need this functionality built into your app. Essentially this is similar to the type of output you get from ChatGPT or CoPilot which frankly might be better options for users for these types of generic queries. Unless you have very specific use cases like the Translation or Summarizing example I showed it's probably best to avoid having generic AI content embedded in your own apps.&lt;/p&gt;
&lt;h4 id="image-generation"&gt;Image Generation&lt;/h4&gt;
&lt;p&gt;Ok the last AI example is a little different in that it creates image output from an input prompt. OpenAI's Dall-E model allows turning text into images.&lt;/p&gt;
&lt;p&gt;Here's an example:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://west-wind.com/wconnect/weblog/imageContent/2025/White-Paper/OpenAI-ImageGeneration.jpg" alt="OpenAI ImageGeneration"&gt;&lt;/p&gt;
&lt;p&gt;The results can be pretty cool like the one above, but it might take more than a few tries to arrive at good useful examples. It's also extremely important that you describe your image in great detail, including some directions on what style and coloring the image should use for example.&lt;/p&gt;
&lt;p&gt;Without more fanfare here's the code to generate an image:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;LPARAMETERS lcPrompt, lcImageFile

do wwDotNetBridge
DO markdownParser

LOCAL loBridge as wwDotNetBridge
loBridge = GetwwDotnetBridge()
loBridge.LoadAssembly(&amp;quot;Westwind.Ai.dll&amp;quot;)

lcOpenAiKey = GETENV(&amp;quot;OPENAI_KEY&amp;quot;)

loConnection = loBridge.CreateInstance(&amp;quot;Westwind.AI.Configuration.OpenAiConnection&amp;quot;)
loConnection.ApiKey = lcOpenAiKey
loConnection.ModelId = &amp;quot;dall-e-3&amp;quot;
loConnection.OperationMode = 1  &amp;amp;&amp;amp; AiOperationModes.ImageGeneration=1

IF EMPTY(lcPrompt)
   lcPrompt = &amp;quot;A Fox that is dressed as a grungy punk rocker, &amp;quot; +;
              &amp;quot;rocking out agressively on an electric guitar. &amp;quot; + ;
              &amp;quot;Use goldenrod colors on a black background in classic poster style format.&amp;quot;	 
ENDIF

loPrompt = loBridge.CreateInstance(&amp;quot;Westwind.AI.Images.ImagePrompt&amp;quot;)
loPrompt.Prompt = lcPrompt

loImageGen = loBridge.CreateInstance(&amp;quot;Westwind.AI.Images.OpenAiImageGeneration&amp;quot;, loConnection)

loEventHandler = CREATEOBJECT(&amp;quot;OpenAICallback&amp;quot;)

*** Pass these so they stay alive and can be accessed in the event handler
loEventHandler.oPrompt = loPrompt
loEventHandler.oImageGen = loImageGen

*** Here we need to match the signature EXACTLY which means ACTUAL enum object
enumOutputFormat = loBridge.GetEnumValue(&amp;quot;Westind.AI.Images.ImageGenerationOutputFormats&amp;quot;,&amp;quot;Url&amp;quot;)
* enumOutputFormat = loBridge.GetEnumValue(&amp;quot;Westind.AI.Images.ImageGenerationOutputFormats&amp;quot;,&amp;quot;Base64&amp;quot;)
loBridge.InvokeTaskMethodAsync(loEventHandler, loImageGen, &amp;quot;Generate&amp;quot;, loPrompt, .F., enumOutputFormat) 

? &amp;quot;Generating Image...&amp;quot;
? &amp;quot;--------------&amp;quot;
? lcPrompt

DEFINE CLASS OpenAICallback as AsyncCallbackEvents

oPrompt = null
oImageGen = null

*** Returns the result of the method and the name of the method name
FUNCTION OnCompleted(llResult,lcMethod)

IF (!llResult)
    ? &amp;quot;Error: &amp;quot; + this.oImageGen.cErrorMsg
    RETURN
ENDIF

lcUrl = this.oPrompt.FirstImageUrl

? &amp;quot;*** Image URL returned by API:&amp;quot;
? lcUrl
?
? &amp;quot;*** Revised Prompt (AI fix-up)&amp;quot;
? this.oPrompt.RevisedPrompt
?

GoUrl(lcUrl)  &amp;amp;&amp;amp; Show the Url

*** Download the image
lcImageFile = &amp;quot;d:\temp\imagegen.png&amp;quot;

*** Download the file using .NET
LOCAL loBridge 
loBridge = GetwwDotnetBridge()
loWebClient = loBridge.CreateInstance(&amp;quot;System.Net.WebClient&amp;quot;)
loWebClient.DownloadFile(lcUrl, lcImageFile)

*** Download file with wwHttp
*!*	DO wwhttp
*!*	loHttp = CREATEOBJECT(&amp;quot;wwHttp&amp;quot;)
*!*	loHttp.Get(lcUrl,  lcImageFile)

GoUrl(lcImageFile)  &amp;amp;&amp;amp; Show the local file

? &amp;quot;*** Done!&amp;quot;
ENDFUNC

* Returns an error message, a .NET Exception and the method name
FUNCTION OnError(lcMessage,loException,lcMethod)
? &amp;quot;Error: &amp;quot; + lcMethod,lcMessage
ENDFUNC

ENDDEFINE
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ImageGeneration uses an &lt;code&gt;ImagePrompt&lt;/code&gt; class that is used to pass in parameters and also retrieve results. You pass in the &lt;code&gt;Prompt&lt;/code&gt; object instance, which is initially populated with the prompt. Upon processing, the Prompt instance is filled with additional data that includes the &lt;code&gt;FirstImageUrl&lt;/code&gt; when generating an image URL, or &lt;code&gt;Base64Data&lt;/code&gt; when generating the raw data. Image generation returns an array of images but the API only ever returns a single image which is returned with &lt;code&gt;FirstImageUrl&lt;/code&gt; or the &lt;code&gt;Base64Data&lt;/code&gt;. You specify which mode - Url or Base64 -  is used via the &lt;code&gt;ImageGenerationsOutputFormats&lt;/code&gt; enum which is passed as the last parameter into the &lt;code&gt;Generate()&lt;/code&gt; async method:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;*** Here we need to match the signature EXACTLY which means ACTUAL enum object
enumOutputFormat = loBridge.GetEnumValue(&amp;quot;Westind.AI.Images.ImageGenerationOutputFormats&amp;quot;,&amp;quot;Url&amp;quot;)
* enumOutputFormat = loBridge.GetEnumValue(&amp;quot;Westind.AI.Images.ImageGenerationOutputFormats&amp;quot;,&amp;quot;Base64&amp;quot;)
loBridge.InvokeTaskMethodAsync(loEventHandler, loImageGen, &amp;quot;Generate&amp;quot;, loPrompt, .F.,enumOutputFormat)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After generation &lt;code&gt;ImageGeneration()&lt;/code&gt; also sets a &lt;code&gt;RevisedPrompt&lt;/code&gt; which is an modified prompt that AI engine fixes up before rendering the image. The prompt tends to be &lt;em&gt;embellished with additional information and styling&lt;/em&gt; which is sometime pretty hilarious - and at other times can trigger the safety triggers of the AI engine, which is not so funny.&lt;/p&gt;
&lt;p&gt;To get an idea of the Prompt and its many properties, look at ILSpy:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://west-wind.com/wconnect/weblog/imageContent/2025/White-Paper/ImagePromptInDotPeekDecompiler.png" alt="Image Prompt In Dot Peek Decompiler"&gt;&lt;/p&gt;
&lt;p&gt;In the code example above I returned the URL so the code displays the URL using &lt;code&gt;GoUrl()&lt;/code&gt; (from &lt;code&gt;wwUtils.prg&lt;/code&gt;)  to display the image in the browser. For bonus points the result handler also downloads the image using .NET's &lt;code&gt;System.Net.WebClient&lt;/code&gt; and storing it in a file. Alternately you can also use &lt;code&gt;wwHttp&lt;/code&gt; if you have it using FoxPro only code.&lt;/p&gt;
&lt;p&gt;For returning base64, we can ask for the image to be returned as base64, which is an HTML style image embedding url (&lt;code&gt;data:image/png;base64,&amp;lt;largeBase64Text&amp;gt;&lt;/code&gt;) , which can be parsed to binary or by using the convenient &lt;code&gt;loPrompt.SaveImageFromBase64(lcFileToSaveTo)&lt;/code&gt; provided by the .NET library. Both Url and base64 work, but base64 is slower as it has to immediately download the image, while URL can just display the URL. URLs are stored only for a limited time on OpenAI, so you'll want to download the URLs if you want to keep any images as soon as you know you want to hang on to them.&lt;/p&gt;
&lt;p&gt;There are two programs that demonstrate both URL and base64 downloads:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;OpenAiImage.prg&lt;/code&gt; (Url)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;OpenAiImageBase64.prg&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="how-do-i-use-this"&gt;How do I use this?&lt;/h4&gt;
&lt;p&gt;The features I've described here I've actually integrated in one of my tools Markdown Monster. Markdown Monster has support for Text Summaries, Translation, Grammar Checking and Image AI generation.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://west-wind.com/wconnect/weblog/imageContent/2025/White-Paper/OpenAI-MarkdownMonster-AIOperations.png" alt="OpenAI MarkdownMonster AIOperations"&gt;&lt;/p&gt;
&lt;p&gt;Here's an example of the AI based Grammar check of a text selection:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://west-wind.com/wconnect/weblog/imageContent/2025/White-Paper/OpenAI-MarkdownMonster-GrammarCheck.png" alt="OpenAI MarkdownMonster GrammarCheck"&gt;&lt;/p&gt;
&lt;p&gt;Similar specialized dialogs exist for translations, and text summaries that can be embedded or replace existing text.&lt;/p&gt;
&lt;p&gt;Image generation is integrated with an interactive editor and image manager that can be used to capture and embed images:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://west-wind.com/wconnect/weblog/imageContent/2025/White-Paper/OpenAI-MarkdownMonster-ImageGeneration.jpg" alt="OpenAI MarkdownMonster ImageGeneration"&gt;&lt;/p&gt;
&lt;p&gt;All of these features are implemented using the same library I've shown in the samples above, so these examples are real world integrations that are in production in a commercial application.&lt;/p&gt;
&lt;h4 id="ai-summary"&gt;AI Summary&lt;/h4&gt;
&lt;p&gt;Using this AI library makes short work of integrating AI functionality into applications. What I've shown are kind of boring, but practical applications of AI functionality that I've actually used and which is probably a good use of this technology.&lt;/p&gt;
&lt;p&gt;Keep in mind that in the current state, AI serves best as an &lt;strong&gt;assistant&lt;/strong&gt; that &lt;strong&gt;helps&lt;/strong&gt; with existing tasks and operations, rather than as a reliable automation tool.&lt;/p&gt;
&lt;p&gt;Although there's tons of talk how AI will eat the world, finding more innovative usage for this tech is not easy, because it's really hard to predict exactly what the result from an AI queries look like. As you've seen even with these simple examples, results can vary wildly between runs, &lt;strong&gt;even with the same exact data&lt;/strong&gt; and even with very simple deterministic requests.&lt;/p&gt;
&lt;p&gt;So, play around with AI features, but be mindful of how reliable the tools are for the job that you need it to actually perform, before jumping in over your head.&lt;/p&gt;
&lt;h3 id="create-a-net-component-and-call-it-from-foxpro"&gt;Create a .NET Component and call it from FoxPro&lt;/h3&gt;
&lt;p&gt;In the course of the examples I've shown there were a couple (&lt;a href="#create-a-net-component-and-call-it-from-foxpro"&gt;wwDotnetBridge 101&lt;/a&gt; and &lt;a href="#humanize-numbers-dates-measurements"&gt;Humanizer&lt;/a&gt;) where I demonstrated using a .NET class of our own to create logic. Let's take a look and see how we can actually do that, using the new and much lighter weight .NET SDK tools, that require no specific tools.&lt;/p&gt;
&lt;p&gt;The good news is that there's now a .NET SDK that you can download that includes everything you need to compile, build and run .NET applications. It's a single, couple of minutes install and you're ready to start creating .NET code. All you really need is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;.NET SDK (&lt;a href="https://dotnet.microsoft.com/en-us/download"&gt;download&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;A text editor&lt;/li&gt;
&lt;li&gt;A Powershell or Command Terminal Window&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You don't need Visual Studio any longer, although if you plan on doing serious .NET development and you want to effectively debug components you build you may still want to use it. Regardless though you can use the SDK and Visual Studio - or any other .NET tools side by side interchangeably.&lt;/p&gt;
&lt;blockquote&gt;
&lt;h5 id="--its-not-your-windows-net-anymore"&gt;&lt;i class="fas fa-lightbulb" style="font-size: 1.1em"&gt;&lt;/i&gt;  It's not your Windows .NET anymore&lt;/h5&gt;
&lt;p&gt;Part of the reason for this shift to more generic tooling is that .NET is no longer a Windows only platform - you can build .NET applications for Mac and Linux. In fact any non-UI and Web applications you build tend to be completely cross-platform out of the box unless you explicitly add platform specific features (ie. Windows API calls, or UI features).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;For purposes of this discussion, the goal is to create a .NET Class library that we can call from FoxPro, which means we want to create a new &lt;strong&gt;Class Library&lt;/strong&gt; project.&lt;/p&gt;
&lt;h4 id="creating-new-projects"&gt;Creating new Projects&lt;/h4&gt;
&lt;p&gt;.NET now lets you create new projects from the command line via the &lt;code&gt;dotnet new&lt;/code&gt; command.&lt;/p&gt;
&lt;p&gt;For FoxPro usage we preferably create projects for .NET Framework - which targets &lt;code&gt;net472&lt;/code&gt; or &lt;code&gt;net481&lt;/code&gt; (4.7.2 recommended since it covers a wider ranger of stock installations).&lt;/p&gt;
&lt;p&gt;Unfortunately the SDK does not allow creating .NET Framework projects directly,  &lt;strong&gt;even though the SDK certainly supports building NET Framework projects&lt;/strong&gt;. The problem is that newer project types use features that aren't supported for .NET framework, so while you can easily create .NET Core projects and make a few changes, it's a bit of a pain.&lt;/p&gt;
&lt;p&gt;The closest you'll get is by targeting &lt;code&gt;netstandard2.0&lt;/code&gt;. .NET Standard works with .NET Framework, but ideally you want to target &lt;code&gt;net472&lt;/code&gt; so we'll end up using &lt;code&gt;netstandard2.0&lt;/code&gt; and changing the target.&lt;/p&gt;
&lt;p&gt;Let's start by going to a parent folder into which we want to create a new project (for samples, into the &lt;code&gt;dotnet&lt;/code&gt; folder to create &lt;code&gt;dotnet\FoxInterop&lt;/code&gt;)&lt;/p&gt;
&lt;p&gt;Make sure you explicitly specify the target framework and use &lt;code&gt;-f netstandard2.0&lt;/code&gt; as it's the only bare bones project type that's close to what you want for .NET framework.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-powershell"&gt;# go to a parent folder - project is created below
dotnet new classlib -n FoxInterop -f netstandard2.0
cd FoxInterop

# Open an editor in the folder - VS Code here
code .  
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here's what you'll see:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://west-wind.com/wconnect/weblog/imageContent/2025/White-Paper/VsCodeProjectNetstandard.png" alt="Vs Code Project Netstandard"&gt;&lt;/p&gt;
&lt;p&gt;So you'll want to change the target from &lt;code&gt;netstandard2.0&lt;/code&gt; to &lt;code&gt;net472&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-xml"&gt;&amp;lt;Project Sdk=&amp;quot;Microsoft.NET.Sdk&amp;quot;&amp;gt;

  &amp;lt;PropertyGroup&amp;gt;
    &amp;lt;TargetFramework&amp;gt;net472&amp;lt;/TargetFramework&amp;gt;
  &amp;lt;/PropertyGroup&amp;gt;

&amp;lt;/Project&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The class generated is empty.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-cs"&gt;using System;

namespace FoxInterop
{
    public class Class1
    {

    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And now you're off to the races.&lt;/p&gt;
&lt;h4 id="output-to-a-specific-folder"&gt;Output to a specific Folder&lt;/h4&gt;
&lt;p&gt;If you're building for FoxPro you'll likely want your .NET output to go to a specific. It turns out that by default output is created in a complex folder structure below the project.&lt;/p&gt;
&lt;p&gt;If you'd rather put the output in your application's output folder (or a bin folder as I like to do for me dependencies) you can change the project file to the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-xml"&gt;&amp;lt;Project Sdk=&amp;quot;Microsoft.NET.Sdk&amp;quot;&amp;gt;

  &amp;lt;PropertyGroup&amp;gt;    
    &amp;lt;TargetFramework&amp;gt;net472&amp;lt;/TargetFramework&amp;gt;    

    &amp;lt;!-- Optional: Output to a specific folder --&amp;gt;    
    &amp;lt;OutputPath&amp;gt;..\..\bin&amp;lt;/OutputPath&amp;gt;
    &amp;lt;AppendTargetFrameworkToOutputPath&amp;gt;false&amp;lt;/AppendTargetFrameworkToOutputPath&amp;gt;    

  &amp;lt;/PropertyGroup&amp;gt;
&amp;lt;/Project&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this case I dump the output directly into my bin folder where the samples can find it.&lt;/p&gt;
&lt;h4 id="creating-a-sample-class"&gt;Creating a Sample Class&lt;/h4&gt;
&lt;p&gt;To create something semi-useful lets create a .NET class and see if we can compile it and access it from FoxPro:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;namespace FoxInterop
{
    public class Person
    {
        public string Name { get; set; } =  &amp;quot;Jane Doe&amp;quot;;
        public string Company { get; set; } = &amp;quot;Acme Inc.&amp;quot;;
        public DateTime Entered { get; set; } = DateTime.UtcNow;
        public Address Address { get; set; } = new Address();

        public override string ToString() 
        {
            return $&amp;quot;{Name} ({Company})\n${Address}&amp;quot;;
        }
    }

    public class Address
    {
        public string Street { get; set; }  =  &amp;quot;123 Main St.&amp;quot;;
        public string City { get; set; }    =  &amp;quot;Anytown&amp;quot;;
        public string State { get; set; }   =  &amp;quot;CA&amp;quot;;
        public string PostalCode { get; set; }  =  &amp;quot;12345&amp;quot;;

        public override string ToString() 
        {
            return $&amp;quot;{Street}, {City}, {State} {PostalCode}&amp;quot;;
        }
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The Person class has a few simple properties as well as a nested &lt;code&gt;Address&lt;/code&gt; object that is automatically initialized. This is good practice to ensure that the &lt;code&gt;Address&lt;/code&gt; is never null even if it can be 'empty'.&lt;/p&gt;
&lt;h4 id="build-the-project"&gt;Build the Project&lt;/h4&gt;
&lt;p&gt;Next you'll need to actually build the project and you can do this from the Terminal via &lt;code&gt;dotnet build&lt;/code&gt; from the project's folder.&lt;/p&gt;
&lt;p&gt;Here's what a sucessful build looks like using the built-in terminal in VsCode:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://west-wind.com/wconnect/weblog/imageContent/2025/White-Paper/VsCodeBuildProject.png" alt="Vs Code Build Project"&gt;&lt;/p&gt;
&lt;p&gt;If the compilation works you'll see a success message, otherwise you'll get an error in the command window along with a line number reference that you can click on:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://west-wind.com/wconnect/weblog/imageContent/2025/White-Paper/VsCodeBuildError.png" alt="Vs Code Build Error"&gt;&lt;/p&gt;
&lt;h4 id="use-it-in-foxpro"&gt;Use it in FoxPro&lt;/h4&gt;
&lt;p&gt;Assuming you get a successful build, the project has built a .NET Assembly (a DLL) into the &lt;code&gt;.\bin&lt;/code&gt; folder where it's accessible to our FoxPro code.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Note that there's &lt;code&gt;_startup.prg&lt;/code&gt; that adds the &lt;code&gt;.\Bin&lt;/code&gt; folder to FoxPro's path and allows .NET assemblies to be loaded out of that folder. All samples call the &lt;code&gt;_startup.prg&lt;/code&gt; just in case it's not explicitly set.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;So now we can create an instance of our .NET class quite simply. You can type the following into the FoxPro command window, or - better - create a small PRG file to run the code.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;CLEAR
DO _startup.prg

DO wwDotnetBRidge
loBridge = GetwwDotnetBridge()
? loBridge.LoadAssembly(&amp;quot;foxInterop.dll&amp;quot;)
loPerson = loBridge.CreateInstance(&amp;quot;FoxInterop.Person&amp;quot;)

*** Access the default property values
? loPerson.Name
? loPerson.Entered

*** Call a method
? loPerson.ToString()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can also access the nested &lt;code&gt;Address&lt;/code&gt; object individually:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;? loPerson.Address.Street
? loPerson.Address.City
? loPerson.Address.ToString()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, in the C# code, let's add a collection of addresses and update the &lt;code&gt;ToString()&lt;/code&gt; code to display those addresses if they are present:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;public List&amp;lt;Address&amp;gt; AlternateAddresses { get; set; } = new List&amp;lt;Address&amp;gt;();

public override string ToString() 
{
    var output =  $&amp;quot;{Name} ({Company})\n${Address}&amp;quot;;

    if (this.AlternateAddresses.Count &amp;gt; 0)
    {
        foreach (var addr in AlternateAddresses)
        {
            output += $&amp;quot;\n${addr}&amp;quot;;
        }
    }
    return output;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now &lt;strong&gt;rebuild the project&lt;/strong&gt; from the Terminal:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-ps"&gt;dotnet build
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But - you likely are running into a problem now, namely that the output assembly is locked, because the FoxPro application has it loaded. So you first have to unload FoxPro (or any running application using the assembly).&lt;/p&gt;
&lt;p&gt;Then do the build again. Now it works!&lt;/p&gt;
&lt;p&gt;Launch FoxPro again and let's add some more code to our test program to add a couple of new alternate addresses.&lt;/p&gt;
&lt;p&gt;Let's start by accessing the AlternateAddresses. Notice that the type is &lt;code&gt;List&amp;lt;Address&amp;gt;&lt;/code&gt; which is a Generic List object. If you recall, Generics cannot be accessed by FoxPro directly, so we have to indirectly access using &lt;code&gt;GetProperty()&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;*** Get ComArray of Address
loAddresses = loBridge.GetProperty(loPerson,&amp;quot;AlternateAddresses&amp;quot;)

? loAddresses.Count &amp;amp;&amp;amp; 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;GetProperty()&lt;/code&gt; in this case returns a &lt;code&gt;ComArray&lt;/code&gt; object which is an object wrapper around .NET collections. The wrapper allows you to read, add, update and delete items in many different array, list, collection and dictionary types.&lt;/p&gt;
&lt;p&gt;In this case we want to add a new item which we can do by using &lt;code&gt;CreateItem()&lt;/code&gt; to create a new object of the list's item type, and &lt;code&gt;Add()&lt;/code&gt; it to the address collection:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;*** Create a new detail items
loAddress = loAddresses.CreateItem()
loAddress.Street = &amp;quot;3123 Nowhere Lane&amp;quot;
loAddress.City = &amp;quot;Nowhere&amp;quot;

*** Add to the list of addresses
loAddresses.Add(loAddress)

loAddress = loAddresses.CreateItem()
loAddress.Street = &amp;quot;43 Somewhere Lane&amp;quot;
loAddress.City = &amp;quot;Somewhere&amp;quot;

*** Add to the list of addresses
loAddresses.Add(loAddress)

*** Print - should include the new address
? loPerson.ToString()
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id="using-net-from-foxpro-is-a-good-choice-for-many-scenarios"&gt;Using .NET from FoxPro is a good Choice for many Scenarios&lt;/h4&gt;
&lt;p&gt;And there you have it - it's pretty simple to create a .NET component and call it from FoxPro.&lt;/p&gt;
&lt;p&gt;This is quite useful if you need to interact with .NET components that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Require lots of code to interact with&lt;/li&gt;
&lt;li&gt;Require complex types that are hard to access from FoxPro&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;As a general rule these days for almost every FoxPro application I create one matching .NET assembly into which I can then easily stuff many small .NET components that can all be called from FoxPro rather than having to figure out how to call various components from FoxPro using wwDotnetBridge indirect access methods. Unless code is super simple, I almost always rather opt for writing in .NET code vs. writing verbose code in FoxPro.&lt;/p&gt;
&lt;p&gt;As a bonus this allows you to get your feet wet with .NET code. Using class library extensions like this is a great way to create small bits of .NET code without having to go full bore of a full conversion. You can gain external functionality that otherwise wouldn't be there while still maintaining the ability to easily edit and recompile the code for changes.&lt;/p&gt;
&lt;p&gt;##AD##&lt;/p&gt;
&lt;h2 id="configuration-and-application-management"&gt;Configuration and Application Management&lt;/h2&gt;
&lt;h3 id="net-configuration-config-files"&gt;.NET Configuration: .config Files&lt;/h3&gt;
&lt;p&gt;The full framework .NET Framework is configured via &lt;code&gt;.config&lt;/code&gt; files that are bound to the executable that launches the application process. This can be confusing for FoxPro applications, because in FoxPro we tend to have two types of launching applications and therefore two different kinds of config files:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;YourApp.exe.config&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Vfp9.exe.config&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;.NET Configuration files are XML based and they look something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-xml"&gt;&amp;lt;?xml version=&amp;quot;1.0&amp;quot;?&amp;gt;
&amp;lt;configuration&amp;gt;
  &amp;lt;startup&amp;gt;   
	&amp;lt;supportedRuntime version=&amp;quot;v4.0&amp;quot; sku=&amp;quot;.NETFramework,Version=v4.7.2&amp;quot; /&amp;gt;	
  &amp;lt;/startup&amp;gt;
  &amp;lt;runtime&amp;gt;
    &amp;lt;loadFromRemoteSources enabled=&amp;quot;true&amp;quot;/&amp;gt;
      
    &amp;lt;assemblyBinding xmlns=&amp;quot;urn:schemas-microsoft-com:asm.v1&amp;quot;&amp;gt;
      &amp;lt;dependentAssembly&amp;gt;
            &amp;lt;assemblyIdentity name=&amp;quot;Newtonsoft.Json&amp;quot; publicKeyToken=&amp;quot;30ad4fe6b2a6aeed&amp;quot; culture=&amp;quot;neutral&amp;quot; /&amp;gt;
            &amp;lt;bindingRedirect oldVersion=&amp;quot;0.0.0.0-13.0.0.0&amp;quot; newVersion=&amp;quot;13.0.0.0&amp;quot; /&amp;gt;
            &amp;lt;/dependentAssembly&amp;gt;
       &amp;lt;/dependentAssembly&amp;gt;
     &amp;lt;/assemblyBinding&amp;gt;     
  &amp;lt;/runtime&amp;gt;
  
&amp;lt;/configuration&amp;gt;  
&lt;/code&gt;&lt;/pre&gt;
&lt;h5 id="supportedruntimeversion"&gt;supportedRuntimeVersion&lt;/h5&gt;
&lt;p&gt;The &lt;code&gt;supportedRuntime&lt;/code&gt; key is not really required any longer as .NET has become very stable. If the supportRuntime is not specified the current installed version is used - typically .NET 4.8.1 these days. This key is used to specify a &lt;strong&gt;minimum supported runtime&lt;/strong&gt; in order for the application to start. If for some reason some ancient version of .NET is installed prior to 4.7.2 - the app will show a message on startup and not run. These days, any machine that's reasonably current will have the latest .NET version installed and there haven't been any major changes since 4.7.2 that break functionality.&lt;/p&gt;
&lt;h5 id="loadfromremoteresources"&gt;loadFromRemoteResources&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;loadFromRemoteSources&lt;/code&gt; is an important setting - it ensures that the host process can load .NET assemblies from network locations without using an explicit .NET security policy. Native .NET executables don't have to do this, but &lt;strong&gt;Visual FoxPro applications require this setting&lt;/strong&gt; due to the way the runtime is hosted.&lt;/p&gt;
&lt;h5 id="assemblybinding"&gt;assemblyBinding&lt;/h5&gt;
&lt;p&gt;Finally the &lt;code&gt;assemblyBinding&lt;/code&gt; section can be used to resolve assembly dependency version mismatches by specifying a specific version of an assembly that should be used regardless of the version that is requested by a dependency. More on that in the next section.&lt;/p&gt;
&lt;p&gt;There are many more configuration settings that are supported in the &lt;code&gt;.config&lt;/code&gt; file. Any .NET settings that are supported via &lt;code&gt;app.config&lt;/code&gt; configuration can also be used in your FoxPro applications. For example, you can use &lt;code&gt;appSettings&lt;/code&gt; for configuration settings, set trace settings, output tracelogs to disk and much more. Possible but probably not something that you need to do for FoxPro integrations - but it's available if you need it.&lt;/p&gt;
&lt;h3 id="net-dependency-version-management"&gt;.NET Dependency Version Management&lt;/h3&gt;
&lt;p&gt;One issue that you can run into and that's somewhat important is one of versioning of depdencies. When you use .NET components you may end up using dependencies that are used by multiple components with each version depending on a different version.&lt;/p&gt;
&lt;p&gt;.NET - and especially .NET framework - is determinisitic about versions it expects so when a component is bind to a component of a certain type by default it expects that specific version to be present. If it's not you may end up getting assembly load errors, that complain that one or another assembly could not resolve its dependencies.&lt;/p&gt;
&lt;p&gt;This situation is better in .NET Core which automatically rolls up dependencies to the highest version, but in .NET Framework you have to do this manually via configuration called Assembly Redirects.&lt;/p&gt;
&lt;p&gt;Assembly redirects let you specify a minimum version and desired higher version. By having a redirect all components requesting a specific versions are then 'redirected' to the higher version and so all can share the same version safely.&lt;/p&gt;
&lt;p&gt;Assembly redirects are set in &lt;code&gt;.config&lt;/code&gt; files. For FoxPro that means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;YourApp.exe.config&lt;/li&gt;
&lt;li&gt;VFP9.exe.config&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;and here is what Assembly Redirects look like if they are needed:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-xml"&gt;&amp;lt;?xml version=&amp;quot;1.0&amp;quot;?&amp;gt;
&amp;lt;configuration&amp;gt;
  &amp;lt;startup&amp;gt;   
	&amp;lt;supportedRuntime version=&amp;quot;v4.0&amp;quot; sku=&amp;quot;.NETFramework,Version=v4.8&amp;quot; /&amp;gt;	
  &amp;lt;/startup&amp;gt;
  &amp;lt;runtime&amp;gt;
    &amp;lt;loadFromRemoteSources enabled=&amp;quot;true&amp;quot;/&amp;gt;
      
    &amp;lt;assemblyBinding xmlns=&amp;quot;urn:schemas-microsoft-com:asm.v1&amp;quot;&amp;gt;
      &amp;lt;dependentAssembly&amp;gt;
            &amp;lt;assemblyIdentity name=&amp;quot;Newtonsoft.Json&amp;quot; publicKeyToken=&amp;quot;30ad4fe6b2a6aeed&amp;quot; culture=&amp;quot;neutral&amp;quot; /&amp;gt;
            &amp;lt;bindingRedirect oldVersion=&amp;quot;0.0.0.0-13.0.0.0&amp;quot; newVersion=&amp;quot;13.0.0.0&amp;quot; /&amp;gt;
            &amp;lt;/dependentAssembly&amp;gt;
            
            &amp;lt;dependentAssembly&amp;gt;
            &amp;lt;assemblyIdentity name=&amp;quot;System.Runtime.CompilerServices.Unsafe&amp;quot; publicKeyToken=&amp;quot;b03f5f7f11d50a3a&amp;quot; culture=&amp;quot;neutral&amp;quot; /&amp;gt;
            &amp;lt;bindingRedirect oldVersion=&amp;quot;0.0.0.0-6.0.0.0&amp;quot; newVersion=&amp;quot;6.0.0.0&amp;quot; /&amp;gt;
            &amp;lt;/dependentAssembly&amp;gt;
            &amp;lt;dependentAssembly&amp;gt;
            &amp;lt;assemblyIdentity name=&amp;quot;System.Memory&amp;quot; publicKeyToken=&amp;quot;cc7b13ffcd2ddd51&amp;quot; culture=&amp;quot;neutral&amp;quot; /&amp;gt;
            &amp;lt;bindingRedirect oldVersion=&amp;quot;0.0.0.0-4.1.0.0&amp;quot; newVersion=&amp;quot;4.0.1.2&amp;quot; /&amp;gt;
            &amp;lt;/dependentAssembly&amp;gt;
            &amp;lt;dependentAssembly&amp;gt;
            &amp;lt;assemblyIdentity name=&amp;quot;System.Buffers&amp;quot; publicKeyToken=&amp;quot;cc7b13ffcd2ddd51&amp;quot; culture=&amp;quot;neutral&amp;quot; /&amp;gt;
            &amp;lt;bindingRedirect oldVersion=&amp;quot;0.0.0.0-4.1.0.0&amp;quot; newVersion=&amp;quot;4.0.3.0&amp;quot; /&amp;gt;
       &amp;lt;/dependentAssembly&amp;gt;
     &amp;lt;/assemblyBinding&amp;gt;     
  &amp;lt;/runtime&amp;gt;
  
&amp;lt;/configuration&amp;gt;  
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you're using a single component you're unlikely to run into issues, but for all these demos I'm doing here, there are many different versions of components used and so a conflict is more likely and we're seeing it here.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;NewtonSoft.Json&lt;/code&gt; is a very widely used JSON parser in .NET so it's often involved in dependency management fix ups.&lt;/p&gt;
&lt;p&gt;The various &lt;code&gt;System&lt;/code&gt; assemblies are typically caused by components that are using &lt;code&gt;netstandard2.0&lt;/code&gt; instead of explicitly targeting .NET Framework (&lt;code&gt;net472&lt;/code&gt; etc.). If there are multiple components that are using &lt;code&gt;netstandard2.0&lt;/code&gt; it's very likely there will be a version conflict.&lt;/p&gt;
&lt;p&gt;So how do you find these problems? When you get an error with a specific assembly you are loading it will typically tell you which dependency is a problem. You can then use a disassembly like ILSpy to check what version is expected and fix the version.&lt;/p&gt;
&lt;blockquote&gt;
&lt;h5 id="--assembly-versions-is-what-matters-for-assembly-redirects"&gt;&lt;i class="fas fa-warning" style="font-size: 1.1em"&gt;&lt;/i&gt;  Assembly Versions is what matters for Assembly Redirects&lt;/h5&gt;
&lt;p&gt;It's important to note that the versions in the assembly bindings are &lt;strong&gt;Assembly Versions&lt;/strong&gt; &lt;em&gt;not File Versions&lt;/em&gt;. You can find assembly versions in tools like ILSpy by looking at each assembly and looking at the metadata for the assembly. &lt;strong&gt;Don't rely on the File Properties window in Explorer which shows the File version&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://west-wind.com/wconnect/weblog/imageContent/2025/White-Paper/ILSpyVersionNumber.png" alt="IL Spy Version Number"&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Assembly redirects can be a pain to track down, but thankfully they are relatively rare.&lt;/p&gt;
&lt;h3 id="keep-it-simple-use-net-when-wwdotnetbridge-is-a-lot-of-effort"&gt;Keep it simple: Use .NET When wwDotnetBridge is a lot of Effort&lt;/h3&gt;
&lt;p&gt;I want to stress once again, to make sure you don't make things too complicated with trying at all costs to keep code purely in FoxPro. Yes, wwDotnetBridge makes it possible to access most .NET code directly, but trust me when I say that it's much easier to create code with proper Intellisense and the ability to directly access the entire language feature set &lt;strong&gt;directly&lt;/strong&gt; rather than having to make indirect calls from FoxPro.&lt;/p&gt;
&lt;p&gt;As a bonus by doing this you can isolate the .NET Wrappers in a way so that they are FoxPro friendly so that you can directly access and use the .NET component from FoxPro without a wrapper.&lt;/p&gt;
&lt;p&gt;You can create &lt;strong&gt;many components in a single .NET project&lt;/strong&gt;, and .NET assemblies tend to be tiny and very efficient to load. Since you're already going to call into .NET code creating a small wrapper that's FoxPro accessible is going to have virtually no overhead - in fact it'll likely be much more efficient than making indirect calls using wwDotnetBridge.&lt;/p&gt;
&lt;p&gt;Take advantage of .NET.&lt;/p&gt;
&lt;h3 id="get-your-feet-wet-with-net"&gt;Get your Feet Wet with .NET&lt;/h3&gt;
&lt;p&gt;Using components is one of the easiest ways to get your feet wet with some .NET code without having to jump into building an entire application. You can slice off some business functionality into .NET or even just those pieces that you can't directly call from FoxPro otherwise.&lt;/p&gt;
&lt;p&gt;Many of the success stories of people that have migrated have come from starting very small and getting a feel for it with a few small features and then expanding outwards from there. wwDotnetBridge makes it entirely feasible to build a hybrid application that works both with FoxPro and .NET.&lt;/p&gt;
&lt;p&gt;As an example, West Wind Html Help Builder which is a very old FoxPro application integrates heavily with .NET. There are entire sub-components that are written in .NET including the Class and Database Importer, the CHM output generation engine, and large parts of the editor interface. There are even a number of UI Dialogs that are .NET based due to odd behaviors of FoxPro dialogs.&lt;/p&gt;
&lt;p&gt;If this sounds like I'm shilling for .NET - in a way I am, but I do so because it has worked extremely well for me. .NET is the easiest platform that you can directly interface with from FoxPro including for desktop applications. There are many other toolsets like NodeJs, Phython, Java etc. that are popular and while I think they are equally as viable as standalone platforms, they don't have anything like .NET in terms of ease of integration with FoxPro or the vast array of Windows system integration. .NET is really the only high level tooling platform that can easily integrate with FoxPro via easy to use COM Interop.&lt;/p&gt;
&lt;p&gt;So, take advantage of this easy integration, while gaining an enormous window of functionality that lets you continue to use FoxPro...&lt;/p&gt;
&lt;p&gt;##AD##&lt;/p&gt;
&lt;h2 id="summary-2"&gt;Summary&lt;/h2&gt;
&lt;p&gt;Alright that oughta do it! ??&lt;/p&gt;
&lt;p&gt;Let's do a high level sum-up:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Lots of cool .NET features available to integrate into FoxPro&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Many Windows System features are exposed through .NET&lt;/li&gt;
&lt;li&gt;Thousands of open source and 3rd parties are available&lt;/li&gt;
&lt;li&gt;With wwDotnetBridge you can access most of it!&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;wwDotnetBridge makes it EASY!&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Call any .NET components without registration&lt;/li&gt;
&lt;li&gt;Opens up most of .NET to FoxPro&lt;/li&gt;
&lt;li&gt;Access most .NET features including&lt;br&gt;
&lt;em&gt;Generics, Value Types, Collections, Enums, Static members etc.&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;Helpers for types that suck in FoxPro&lt;br&gt;
&lt;em&gt;ComArray, ComValue&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Create Wrappers for Complex Code&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Easier to create .NET code to interface with complex APIs&lt;/li&gt;
&lt;li&gt;Make the wrapper FoxPro friendly&lt;/li&gt;
&lt;li&gt;Call the Wrapper from FoxPro&lt;/li&gt;
&lt;li&gt;Create FoxPro Wrappers for .NET Code&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="resources"&gt;Resources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://webconnection.west-wind.com/docs/_24n1cfw3a.htm"&gt;wwDotnetBridge Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/RickStrahl/swfox2024-wwdotnetbridge-revisited"&gt;GitHub Repository&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;Code Samples&lt;/li&gt;
&lt;li&gt;White Paper&lt;/li&gt;
&lt;li&gt;Slides&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/RickStrahl/wwDotnetBridge"&gt;Get wwDotnetBridge from GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/RickStrahl/swfox2024-wwdotnetbridge-revisited/raw/master/Documents/Strahl-swFox2024-wwDotnetBridge-Revisited.pptx"&gt;Slides for SWFox 2024 Session&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
 &lt;div style="margin-top: 30px;font-size: 0.8em;
            border-top: 1px solid #eee;padding-top: 8px;"&gt;
    &lt;img src="https://markdownmonster.west-wind.com/favicon.png" style="height: 20px;float: left; margin-right: 10px;"&gt;
    this white paper was created and published with the 
    &lt;a href="https://markdownmonster.west-wind.com" target="top"&gt;Markdown Monster Editor&lt;/a&gt; 
&lt;/div&gt;
</description>
     </item>
     <item>
			<title>West Wind Web Connection 8.0 Release Notes</title>
			<pubDate>Wed, 26 Jun 2024 00:05:49 GMT</pubDate>
			<guid isPermaLink="false">9179_20240625</guid>
			<link>https://west-wind.com/wconnect/weblog/ShowEntry.blog?id=9179</link>
			<dc:creator>Rick Strahl</dc:creator>
			<comments>https://west-wind.com/wconnect/weblog/ShowEntry.blog?id=9179#Feedback</comments>
			<slash:comments>0</slash:comments>
			<description>&lt;p&gt;&lt;a href="https://webconnection.west-wind.com"&gt;&lt;img src="https://webconnection.west-wind.com/images/WebConnection_Code_Banner.png" alt="Web Connection Logo"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;It's been a bit and &lt;a href="https://webconnection.west-wind.com"&gt;Web Connection is turning 8.0&lt;/a&gt;! After six years of incremental improvements it's time to rev Web Connection to its next major version.&lt;/p&gt;
&lt;p&gt;Although this is a major version release that has a number of significant updates, there is only one specialized feature (FTP) affected with &lt;a href="https://webconnection.west-wind.com/docs/_s8104dggl.htm#breaking-changes-in-v8.0"&gt;breaking changes&lt;/a&gt;, so if you're upgrading from a v7 version, upgrades should be quick and easy like most other minor version updates.&lt;/p&gt;
&lt;p&gt;For the last few years, the focus of Web Connection has been on continuous, small incremental feature enhancements and improvements around the development and administration process. So rather than huge, disruptive major releases updates, there have been gradual updates that come integrated one small piece at a time to avoid the big version shocks. This version rollover release has a bit more in the update department, but still follows the incremental improvement model and other than a single breaking change with the new FtpClient classes, this release has no breaking changes from recent v7 releases.&lt;/p&gt;
&lt;h2 id="links-and-upgrades"&gt;Links and Upgrades&lt;/h2&gt;
&lt;p&gt;Before we get into what's new, here are the links for the latest release, purchase and upgrade:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://webconnection.west-wind.com"&gt;Web Connection Home&lt;/a&gt;&lt;br&gt;
You can find the download for the Shareware version here.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://store.west-wind.com/product/wconnect80"&gt;Buy a Web Connection License&lt;/a&gt;&lt;br&gt;
Purchase a full developer and 1 server license for Web Connection.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://store.west-wind.com/product/wconnect80_UP"&gt;Buy a Web Connection Upgrade &lt;em&gt;(from any version)&lt;/em&gt;&lt;/a&gt;&lt;br&gt;
Purchase an upgrade for any version of Web Connection. All versions back to 1.x are allowed.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;h5 id="existing-runtime-license-upgrades"&gt;Existing Runtime License Upgrades&lt;/h5&gt;
&lt;p&gt;Runtime licenses don't have specific upgrade SKUs, but can still be upgraded at 50% of the full price. For upgrade runtime license purchases, pick a full runtime license and then apply the Promo Code: RUNTIME_UPGRADE if you qualify. &lt;strong&gt;This only applies only to Runtime Updates&lt;/strong&gt; not to version updates or new purchases. Use this code with a single item of the Runtime you wish to upgrade only.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;h5 id="free-upgrade-if-purchased-after-july-1st-2023"&gt;Free Upgrade if purchased after July 1st, 2023&lt;/h5&gt;
&lt;p&gt;If you purchased Web Connection 7.x on or after July 1st, 2023 you can upgrade for free until the end of 2024. Use promo code: &lt;code&gt;FREE_UPGRADE&lt;/code&gt;.  Use this code on an order with a single item of the Web Connection Upgrade only.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;em&gt;&lt;i class="fas fa-info-circle" style="font-size: 1.1em"&gt;&lt;/i&gt;  &lt;strong&gt;Note&lt;/strong&gt;: Upgrades are always verified and these promo codes &lt;strong&gt;apply only to the specific upgradable item&lt;/strong&gt;.  Please use these specialized Promo Codes only on orders that qualify based on the two descriptions above. If you use these codes with other types of upgrades or orders your order will be rejected. We reserve the right to refuse upgrades based on non-conforming orders.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id="whats-new"&gt;What's new&lt;/h2&gt;
&lt;p&gt;Let's take a look what's new in this release:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#com-server-management-improvements"&gt;COM Server Management Improvements&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#improved-web-connection-server-error-logging"&gt;Improved Web Connection Server Error Logging&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#new-ftpclient-classes"&gt;New FTPClient Classes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#new-wwziparchive-class"&gt;New wwZipArchive Class&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#rest-service-token-authentication-support"&gt;REST Service Token Authentication Support&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#wwrestprocesslrawresponse-helper-property"&gt;wwRestProcess.lRawResponse Helper Property&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#wwdotnetbridge-improvements"&gt;wwDotnetBridge Improvements&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#json-and-rest-service-improvements-in-recent-versions"&gt;JSON and REST Client Improvements&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#wwcache-improvements"&gt;wwCache Improvements&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;More info on what's new in recent releases check out the &lt;a href="https://webconnection.west-wind.com/docs/_s8104dggl.htm"&gt;What's New Page&lt;/a&gt; in the documentation.&lt;/p&gt;
&lt;h2 id="com-server-management-improvements"&gt;COM Server Management Improvements&lt;/h2&gt;
&lt;p&gt;For deployed applications Web Connection should be run in COM mode, and COM mode includes an internal instance pool manager that makes it possible to effectively run FoxPro single threaded servers in a multi-threaded environment with simultaneous request handling. Getting the single threaded (or STA threaded really) FoxPro to behave in pure multi-threaded environment of .NET is a complex matter and involves a lot of trickery to make it work &lt;strong&gt;consistently and reliably&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;In this release the COM Pool manager has seen a major refactoring:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Faster, parallelized Server Loading and Unloading&lt;/li&gt;
&lt;li&gt;Servers are available as soon as they load&lt;/li&gt;
&lt;li&gt;Reliable loading and unloading&lt;/li&gt;
&lt;li&gt;No more double loading or unloading&lt;/li&gt;
&lt;li&gt;All instance exes are released on unload (no more orphaned servers)&lt;/li&gt;
&lt;li&gt;Improved error logging especially in detail mode&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Web Connection has a long history of using COM Servers for production environments and while the technology and the implementation worked really well over the years all the way back to ISAPI, .NET, and now .NET Core, there have always been a few rough edges when it comes to server loading and unloading especially in very busy and high instance environments.&lt;/p&gt;
&lt;p&gt;This release addresses these issue with a completely new pipeline for COM server loading and unloading that is reliable and - as a bonus - much quicker through parallelization of the load and unload processes.&lt;/p&gt;
&lt;p&gt;You can get an idea of load/unload performance in this screen capture which demonstrates 5 server instances under heavy load from a &lt;a href="https://websurge.west-wind.com/"&gt;West Wind WebSurge&lt;/a&gt; load test run, with the server pool constantly being loaded, unloaded, run as a single instance and the Application Pool being restarted:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://github.com/RickStrahl/ImageDrop/blob/master/WebConnection/ServerLoading.gif?raw=true" alt="Web Connection Server Loading and Unloading"&gt;&lt;br&gt;
&lt;small&gt;&lt;strong&gt;Figure 1&lt;/strong&gt; - Web Connection COM Server Loading and Unloading Improvements&lt;/small&gt;&lt;/p&gt;
&lt;p&gt;You can see that server loading is very fast, and if you look closely you can see instances immediately processing requests as the servers load, while the rest of the pool is still loading.&lt;/p&gt;
&lt;p&gt;Servers are now loaded in parallel rather than sequentially which results in servers loading much quicker than previously. Additionally, servers are available immediately as soon as they enter the pool, while others are still loading. Previously the load process was blocked and sequential loading caused a potentially significant delay before processing could start. This doesn't matter much if you're running a few instances, but if you're running 10 or as many as 40 instances as one of our customers does, startup time can be a significant issue.&lt;/p&gt;
&lt;p&gt;Additionally we fixed some related issues that in some cases caused double loading of server instances. Because the COM server load process first unloads before loading new instances, it was possible previously to end up in a scenario where instances were asking to unload while new instances where already loading. All of these issues have been addressed in the latest release with some creative thread management - ah, the joys of multi-threaded development ??&lt;/p&gt;
&lt;p&gt;The changes have been made both the Web Connection .NET Module and the Web Connection .NET Core Web Server.&lt;/p&gt;
&lt;blockquote&gt;
&lt;h5 id="icon-info-circle-the-web-connection-net-core-web-server"&gt;&lt;i class="fas fa-info-circle" style="font-size: 1.1em"&gt;&lt;/i&gt;  The Web Connection .NET Core Web Server&lt;/h5&gt;
&lt;p&gt;The &lt;strong&gt;Web Connection .NET Core Web Server&lt;/strong&gt; was introduced with Web Connection 7.0  primarily as a tool to allow you develop locally without IIS. But it also to allows you &lt;strong&gt;distribute a local Web server with your own applications&lt;/strong&gt; that let you effectively &lt;strong&gt;build and distribute local Web Applications&lt;/strong&gt; that can run on a desktop machine. The .NET Core server middleware also supports running inside of IIS (if you want consistency between dev and production) and can even be used on non-Windows platforms like Linux either as a standalone Web server or a behind a reverse proxy server like NginX &lt;em&gt;(but the FoxPro code still has to run on Windows)&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="improved-web-connection-server-error-logging"&gt;Improved Web Connection Server Error Logging&lt;/h2&gt;
&lt;p&gt;In the process of updating the Web Connection server connectors we've also reviewed and updated the server logging that goes into the &lt;code&gt;wcerrors.txt&lt;/code&gt; logs. We've cleaned up the error logging so that non-detail mode doesn't log anything but critical messages and errors - previously there were some ambiguous trace messages that often came up in discussion on the forums, but weren't actual errors. These have been removed, and you should now see a mostly blank &lt;code&gt;wcerrors.txt&lt;/code&gt; file in normal operation, &lt;strong&gt;except if you start having problems with your servers&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Detail logging (&lt;code&gt;LogDetail&lt;/code&gt; true in the configuration) logs a lot of error and non-error information into the log including request start and stop operations, request timings, application start and much more detailed error information on errors.&lt;/p&gt;
&lt;p&gt;Detail mode now always shows the request id and thread information  (if available) to more easily correlate requests in a busy error log.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://support.west-wind.com/PostImages/2024/_DeXLe3mgo4PCybNL.png" alt="Web Connection Error Logging"&gt;&lt;br&gt;
&lt;small&gt;&lt;strong&gt;Figure 2&lt;/strong&gt; - Web Connection Detail Error Logging &lt;/small&gt;&lt;/p&gt;
&lt;p&gt;You'll also notice that the actual request completion or call error is marked with a &lt;code&gt;*** &lt;/code&gt; prefix so it's more easily visible in the noise. The &lt;code&gt;***&lt;/code&gt; entries are either a completed request or the actual request processing error message that occurred during the COM call.&lt;/p&gt;
&lt;h2 id="new-ftpclient-classes"&gt;New FTPClient Classes&lt;/h2&gt;
&lt;p&gt;This release has a completely new set of FTPClient classes that replace the old &lt;code&gt;wwFtp&lt;/code&gt; and &lt;code&gt;wwSFTP&lt;/code&gt; classes. The new version uses a .NET based interface instead of the legacy WinInet features that are somewhat limited in that they didn't support the &lt;code&gt;FTPS&lt;/code&gt; (FTP over TLS) protocol which to be frank makes them useless in today's environment where secure connections are a requirement.&lt;/p&gt;
&lt;p&gt;The new version relies on two .NET libraries:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;FluentFtp  (for FTP and FTPS)&lt;/li&gt;
&lt;li&gt;SSH.NET (for SFTP)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We've been using SSH.NET previously for SFTP support, but FluentFtp integration is new, and it provides for the new &lt;code&gt;FTPS&lt;/code&gt; support in Web Connection (and the &lt;a href="https://client-tools.west-wind.com"&gt;West Wind Client Tools&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;The new classes are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://webconnection.west-wind.com/docs/_6wp0mrz80.htm"&gt;wwFtpClient&lt;/a&gt; (FTP and FTPS)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://webconnection.west-wind.com/docs/_6wr0zm6jd.htm"&gt;wwSFtpClient&lt;/a&gt; (SFTP)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The two classes have the exact same API surface except for connection information which is slightly different for SFTP which requires SSH keys or configuration files instead of username and password for standard FTP.&lt;/p&gt;
&lt;p&gt;The new classes follow a similar interface to the old connection based  &lt;code&gt;wwFTP&lt;/code&gt;/&lt;code&gt;wwFTPS&lt;/code&gt; classes, so if you used them with &lt;code&gt;Connect()&lt;/code&gt;... FTP Operation... &lt;code&gt;Close()&lt;/code&gt; operations the syntax will be identical and easy to upgrade. In most cases you should be able to simply change the class name - ie. change &lt;code&gt;CREATEOBJECT(&amp;quot;wwFtp&amp;quot;)&lt;/code&gt; to &lt;code&gt;CREATEOBJECT(&amp;quot;wwFtpClient&amp;quot;)&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;What's missing from the old &lt;code&gt;wwFtp&lt;/code&gt; and &lt;code&gt;wwsftp&lt;/code&gt; classes are the single method FTP operations for uploading and downloading. These were awkward to use with their long parameter lists anyway and the class based interface is cleaner to use anyway. The old &lt;code&gt;wwFtp&lt;/code&gt; and &lt;code&gt;wwSftp&lt;/code&gt; classes are still shipped in the &lt;code&gt;\classes\OldFiles&lt;/code&gt; folder and can still be used - just copy them into your path and they'll work like before.&lt;/p&gt;
&lt;p&gt;To demonstrate the new FtpClient functionality, here's an example that runs through most operations supported:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;CLEAR
DO wwFtpClient
DO wwUtils   &amp;amp;&amp;amp; for display purposes only

loFtp  = CREATEOBJECT(&amp;quot;wwFtpClient&amp;quot;)
loFtp.lUseTls = .T.
loFtp.cLogFile = &amp;quot;c:\temp\ftp.log&amp;quot; &amp;amp;&amp;amp; verbose log - leave empty
loFtp.lIgnoreCertificateErrors = .F.   &amp;amp;&amp;amp; self-signed cert not installed


*** cServer can be &amp;quot;someserver.com&amp;quot;, &amp;quot;someserver.com:22&amp;quot;, &amp;quot;123.213.222.111&amp;quot;
lcServer =  INPUTBOX(&amp;quot;Server Domain/IP&amp;quot;)
IF EMPTY(lcServer)
   RETURN
ENDIF

lcUsername = InputBox(&amp;quot;User name&amp;quot;)
IF EMPTY(lcUsername)
   RETURN
ENDIF

lcPassword = GetPassword(&amp;quot;Password&amp;quot;)
IF EMPTY(lcPassword)
	RETURN
ENDIF

*** Progress Events - class below
loFtp.oProgressEventObject = CREATEOBJECT(&amp;quot;FtpClientProgressEvents&amp;quot;)

loFtp.cServer = lcServer
loFtp.cUsername = lcUsername
loFtp.cPassword = lcPassword
*loFtp.nPort = 21 &amp;amp;&amp;amp; only needed if custom port is required

IF !loFtp.Connect()
	? loFtp.cErrorMsg
	RETURN
ENDIF
? &amp;quot;Connected to &amp;quot; + lcServer	

loFtp.Exists(&amp;quot;Tools/jsMinifier1.zip&amp;quot;)


IF !loFtp.DownloadFile(&amp;quot;Tools/jsMinifier.zip&amp;quot;, &amp;quot;c:\temp\jsMinifier.zip&amp;quot;)
	? loFtp.cErrorMsg
	RETURN
ENDIF	
? &amp;quot;Downloaded &amp;quot; + &amp;quot;Tools/jsMinifier.zip&amp;quot;

lcUploadFile = &amp;quot;Tools/jsMinifier&amp;quot; + SYS(2015) + &amp;quot;.zip&amp;quot;
IF !loFtp.UploadFile(&amp;quot;c:\temp\jsMinifier.zip&amp;quot;, lcUploadFile)
	? loFtp.cErrorMsg
	RETURN
ENDIF
? &amp;quot;Uploaded &amp;quot; + lcuploadFile

*** provide a folder name (no wildcards)
loCol = loFtp.ListFiles(&amp;quot;/Tools&amp;quot;)
IF ISNULL(locol)
   ? &amp;quot;Error: &amp;quot; + loFtp.cErrorMsg
   RETURN
ENDIF   
? TRANSFORM(loCol.Count ) + &amp;quot; matching file(s)&amp;quot;
? loFtp.cErrorMsg
FOR EACH loFile IN loCol FOXOBJECT
   IF ( AT(&amp;quot;jsMinifier_&amp;quot;,loFile.Name) = 1)
	   ? loFtp.oBridge.ToJson(loFile)  &amp;amp;&amp;amp; for kicks print out as json
	   IF loFtp.DeleteFile(loFile.FullName)
	      ? &amp;quot;Deleted &amp;quot; + loFile.FullName
	   ENDIF
	   
   ENDIF
ENDFOR

loFiles = loFtp.ListFiles(&amp;quot;/Tools&amp;quot;)
FOR EACH loFile in loFiles
   ? loFile.Name + &amp;quot; &amp;quot; + TRANSFORM(loFile.LastWriteTime)
ENDFOR

* loFtp.Close()  &amp;amp;&amp;amp; automatic when released

RETURN


DEFINE class FtpClientProgressEvents as Custom

FUNCTION OnFtpBufferUpdate(lnPercent, lnDownloadedBytes, lcRemotePath, lcMode)
  lcMsg = lcMode + &amp;quot;: &amp;quot; + TRANSFORM(lnPercent) + &amp;quot;% complete. &amp;quot; + lcRemotePath + &amp;quot; - &amp;quot; + TRANSFORM(lnDownloadedBytes) + &amp;quot; bytes&amp;quot;
  ? &amp;quot;*** &amp;quot; + lcMsg
ENDFUNC

ENDDEFINE 
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="new-wwziparchive-class"&gt;&lt;a href="https://webconnection.west-wind.com/docs/_6ww0y5kqq.htm"&gt;New wwZipArchive Class&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This release also has a new ZipArchive class that provides more control over zip functionality using modern, native .NET and built-in functionality that removes the old dependency on Dynazip libraries.&lt;/p&gt;
&lt;p&gt;The new class provides the ability to add files to existing zip files and iterate and retrieve files individually.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;CLEAR
DO wwZipArchive

loZip = CREATEOBJECT(&amp;quot;wwZipArchive&amp;quot;)
lcZipFile = &amp;quot;d:\temp\zipFiles.zip&amp;quot;

*** Zip up a folder with multiple wildcards
*** 
IF !loZip.ZipFiles(;
   lcZipFile,;
   &amp;quot;*.fpw,*.vc?,*.dll,*.h&amp;quot;,;  
   CURDIR(),;
   .T., .T.)
   ? &amp;quot;Zipping Error: &amp;quot; + loZip.cErrorMsg
   RETURN
ENDIF   
? loZip.cErrorMsg
   
*** add a single file   
IF !loZip.AppendFiles(lcZipFile, &amp;quot;wwZipArchive.prg&amp;quot;)
   ? &amp;quot;Error: &amp;quot; + loZip.cErrorMsg
   RETURN
ENDIF
   
*** Unzip into a folder   
IF !loZip.UnzipFolder(lcZipFile, &amp;quot;d:\temp\Unzipped1&amp;quot;)
   ? &amp;quot;Unzip Error: &amp;quot; + loZip.cErrorMsg
   RETURN
ENDIF

*** Look at all files in the zip   
loEntries = loZip.GetZipEntries(lcZipFile)
IF ISNULL(loEntries)
   ? &amp;quot;No entries:  &amp;quot; + loZip.cErrorMsg
   RETURN 
ENDIF
? loEntries.Count

*** Iterate through the collection
FOR EACH loEntry IN loEntries FoxObject     
	? loEntry.Name + &amp;quot;  &amp;quot; + ;
	  loEntry.Fullname + &amp;quot; &amp;quot; + ;
	  TRANSFORM(loEntry.Length) + &amp;quot; - &amp;quot; + ;
	  TRANSFORM(loEntry.CompressedLength)
ENDFOR   

*** Unzip an individual entry and unzip it - first in this case
loZip.UnzipFile(lcZipFile, loEntries[1].FullName, &amp;quot;d:\temp\&amp;quot; + loEntries[1].Name)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These functions use wwDotnetBridge and built-in .NET framework features for zipping files. Note that one thing missing here is support for encrypted Zip files which is not supported by the .NET APIs.&lt;/p&gt;
&lt;p&gt;The old &lt;code&gt;ZipFiles()&lt;/code&gt; and &lt;code&gt;UnzipFiles()&lt;/code&gt; in &lt;code&gt;wwAPI.prg&lt;/code&gt; are still available as well, but you need to make sure you have the &lt;code&gt;dzip.dll&lt;/code&gt; and &lt;code&gt;dunzip.dll&lt;/code&gt; files available in your distribution.&lt;/p&gt;
&lt;h2 id="rest-service-token-authentication-support"&gt;&lt;a href="https://webconnection.west-wind.com/docs/_6rc0lbyfk.htm"&gt;REST Service Token Authentication Support&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Unlike standard wwProcess classes, wwRestProcess does not work with standard session cookies and by default all session support is turned off. However, you can enable session support via Bearer Token authentication which reads a user supplied identity token from the Authorization HTTP header.&lt;/p&gt;
&lt;p&gt;There are two mechanisms available:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://webconnection.west-wind.com/docs/_6r60zs1wr.htm"&gt;InitTokenSession&lt;/a&gt;&lt;br&gt;
This method is the core feature that ties a generated token to a Web Connection wwSession object via its cSessionId parameter. &lt;code&gt;InitTokenSession()&lt;/code&gt; either retrieves an existing session from a provided Bearer token, or if one isn't provided or matched provides an empty session. To create a new Token you can have a custom sign in method and call NewSession() to map your custom user/customer/etc. to a session with a session holding any additional data.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://webconnection.west-wind.com/docs/_6r619bpu9.htm"&gt;Authenticate()&lt;/a&gt;&lt;br&gt;
If you want basic mapping of the session to a user in a similar way to the way HTML authentication works with cookies you can use the Authenticate() method which serves a dual purpose for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Validating a wwSession Token and Loading a User&lt;/li&gt;
&lt;li&gt;Authenticating user credentials&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;InitTokenSession()&lt;/code&gt; is the low level function that checks for bearer tokens and maps them onto a wwSession object. It generates new tokens on every request but only stores them if you explicitly save them in a sign in request. To see if a user has a previous approved token you can check &lt;code&gt;!Session.lIsNewSession&lt;/code&gt;. This is pretty low level but provides to core feature of token management and whether a user has a token that matches an existing token.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;FUNCTION OnProcessInit
...

*** Pick up existing token or create a new token
*** and set on the oSession object
THIS.InitTokenSession()

*** Define anonymous requests that don't need validation
lcScriptName = LOWER(JUSTFNAME(Request.GetPhysicalPath()))
llIgnoreLoginRequest = INLIST(lcScriptName,&amp;quot;testpage&amp;quot;,&amp;quot;signin&amp;quot;)

*** Fail if no token and not a passthrough request
IF !llIgnoreLoginRequest AND this.oSession.lIsNewSession
   THIS.ErrorResponse(&amp;quot;Access Denied. Please sign in first.&amp;quot;,&amp;quot;401 Unauthorized&amp;quot;)
   RETURN .F.
ENDIF

RETURN .T.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Authenticate()&lt;/code&gt; maps on top of that functionality by taking a mapped token and mapping it to an UserSecurity object, providing all the familiar User Security features like the &lt;code&gt;.oUser&lt;/code&gt;, &lt;code&gt;lIsAuthenticated&lt;/code&gt;, &lt;code&gt;cAuthenticatedUser&lt;/code&gt; etc. properties on the &lt;code&gt;wwProcess&lt;/code&gt; class.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;FUNCTION OnProcessInit
...

*** IMPORTANT: InitTokenSession is required to pick up the Bearer token
***            and load or create a new session
THIS.InitTokenSession()

*** Check for pages that should bypass auth - signin always (not signout though!)
lcScriptName = LOWER(JUSTFNAME(Request.GetPhysicalPath()))
llIgnoreLoginRequest = INLIST(lcScriptName,&amp;quot;testage&amp;quot;,&amp;quot;signin&amp;quot;)

IF !llIgnoreLoginRequest
   *** Check for Authentication here based on the token (note no parameters)
   IF !this.Authenticate()   
	   THIS.ErrorResponse(&amp;quot;Access Denied. Please sign in first.&amp;quot;,&amp;quot;401 Unauthorized&amp;quot;)
	   RETURN .F. &amp;amp;&amp;amp; Response is handled
   ENDIF
ENDIF

*** One you're here you can now acccess these anywhere in your process code:
llIsLoggedin = this.lIsAuthenticated
lcUsername = this.cAuthenticatedUser
loUser = this.oUser

RETURN .T.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Choose the &lt;code&gt;Authenticate()&lt;/code&gt; approach if you need to know who your users are explicitly. Use the &lt;code&gt;InitTokenSession()&lt;/code&gt; if you only need to know that they are have signed in and are validated. &lt;code&gt;Authenticate()&lt;/code&gt; tries to map the token to a user and there are several overloads of this method with various parameter signatures. You can also override these methods with custom behavior for mapping users to tokens.&lt;/p&gt;
&lt;p&gt;Beyond those two approaches you still need to actually validate a user via some sort of sign in operation that authenticates a user and then creates the actual token.  This can be another endpoint or it could be an oAuth operation or even a standard Web page.&lt;/p&gt;
&lt;p&gt;The following uses a REST endpoint in an existing API (ie. part of the REST service):&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;FUNCTION Signin
LPARAMETER loCredentials

*** Load some business object (or plain DAL code) that can authorize a user
loBus = CREATEOBJECT(&amp;quot;cUser&amp;quot;)

*** Use whatever custom Authorization you need to assign a token
IF !loBus.AuthorizeUser(loCredentials.UserName, loCredentials.Password)
   RETURN THIS.ErrorResponse(loBus.cErrorMsg,&amp;quot;401 Unauthorized&amp;quot;)
ENDIF

*** Create a new Session and optionally assign a mapping user id
*** that links back to a user/customer record in the Application
lcToken = THIS.oSession.NewSession(loBus.oData.UserId)
THIS.oSession.SetSessionVar(&amp;quot;tenant&amp;quot;,loBus.oData.TenantId)
THIS.oSession.SetSessionVar(&amp;quot;displayname&amp;quot;,loBus.oData.dispName)
THIS.oSession.Save()  &amp;amp;&amp;amp; Must explicitly save to Db


*** Return the token and expiration (or whatever you choose)
loToken = CREATEOBJECT(&amp;quot;EMPTY&amp;quot;)
ADDPROPERTY(loToken,&amp;quot;token&amp;quot;, lcToken)
ADDPROPERTY(loToken,&amp;quot;expires&amp;quot;, DATETIME() + 3600 * 24)

RETURN loToken  
* Returns JSON: { token: &amp;quot;&amp;lt;token&amp;gt;&amp;quot;, expires: &amp;quot;2023-10-23T07:00:00Z&amp;quot; }
ENDFUNC
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The focus behind this code is to create a new token with &lt;code&gt;Session.NewSession()&lt;/code&gt; and then saving it into the session table.&lt;/p&gt;
&lt;p&gt;The token is then returned to the client, who will then use it to pass in the &lt;code&gt;Bearer &lt;/code&gt; token &lt;code&gt;Authorization&lt;/code&gt; headers with their REST client requests. Something akin to this in FoxPro code:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;loHttp = CREATEOBJECT(&amp;quot;wwHttp&amp;quot;)
loHttp.AddHeader(&amp;quot;Authorization&amp;quot;,&amp;quot;Bearer &amp;quot; + lcToken)
lcJson = loHttp.Get(lcUrl)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;All of this is designed to make it easier to create REST services that can authenticate without having to re-build a bunch of infrastructure. Instead this stuff re-uses what Web Connection already provides and exposes it to the newer REST service infrastructure with a couple of relative simple constructs you can add to your REST service with a few lines of code.&lt;/p&gt;
&lt;h2 id="wwrestprocesslrawresponse-helper-property"&gt;wwRestProcess.lRawResponse Helper Property&lt;/h2&gt;
&lt;p&gt;Speaking of REST services here's a small, but frequently used feature: There's now a &lt;code&gt;Process.lRawResponse&lt;/code&gt; property that can be set to to &lt;code&gt;.t.&lt;/code&gt; to return a raw, non-JSON response from a REST method. That functionality was always available via the &lt;code&gt;JsonService.IsRawResponse&lt;/code&gt;, but it's a bit easier to set it on the local class instance.&lt;/p&gt;
&lt;p&gt;So you can do the following in a REST method now:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;FUNCTION ReturnPdf()

THIS.lRawResponse = .T.
Response.ContentType = &amp;quot;application/pdf&amp;quot;

 lcFilename = THIS.ResolvePath(&amp;quot;~/policy.pdf&amp;quot;) 

*** Send from memory - string/blob
lcFile = FILETOSTR(lcFilename)
Response.BinaryWrite( lcFile )

*** OR: Send from file
*!* Response.TransmitFile(lcFilename,&amp;quot;application/pdf&amp;quot;)

ENDFUNC
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="wwdotnetbridge-improvements"&gt;wwDotnetBridge Improvements&lt;/h2&gt;
&lt;p&gt;There are a number of small tweaks to wwDotnetBridge as well in this release:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;a href="VFPS://Topic/_6VO0PU6U2"&gt;wwDotnetBridge.GetPropertyRaw()&lt;/a&gt; and &lt;a href="VFPS://Topic/_6VO0P9YVG"&gt;ComArray.ItemRaw()&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
Overridden methods that allow for retrieval of property values in raw format that bypass the usual FoxPro fix-ups that ensure type safe values are returned to FoxPro. Useful in scenarios where the values are sometimes in ComValue or ComArray that can be accessed directly, or in scenarios where types have dual meaning (ie. char with raw number vs. string fix-up or Guid with raw binary vs. string fix-up).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;a href="VFPS://Topic/_6VO0OEM1H"&gt;ComArray.GetInstanceTypeName()&lt;/a&gt; and &lt;a href="VFPS://Topic/_6VO0OF6WL"&gt;ComArray.GetItemTypename()&lt;/a&gt; helpers&lt;/strong&gt;&lt;br&gt;
Added a couple of helpers to the ComArray class to provide type information about the Array instance and it's client types for debugging or testing purposes. This can be useful to determine whether the &lt;code&gt;.Instance&lt;/code&gt; member can be accessed directly via FoxPro code (many .NET collections cannot and require intermediary operations provided by ComArray or wwDotnetBridge).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;a href="VFPS://Topic/_6WZ0YZHBV"&gt;wwDotnetBridge::DisposeInstance() to explicitly release Object Dependencies&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
This method explicitly release &lt;code&gt;IDisposable&lt;/code&gt; object instances by calling &lt;code&gt;.Dispose()&lt;/code&gt;. Since &lt;code&gt;.Dispose()&lt;/code&gt; tends to be an overloaded virtual property you typically can't call it directly on a .NET reference instance, so this method helps making a direct call rather than calling &lt;code&gt;InvokeMethod()&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;a href="VFPS://Topic/_5PJ0XL2YP"&gt;wwDotnetBridge: Improved support for Task Exception Handling&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
When making calls to .NET &lt;code&gt;async&lt;/code&gt; or &lt;code&gt;Task&lt;/code&gt; methods, wwDotnetBridge now does a better job of handling exceptions and returning the result in the &lt;code&gt;OnError()&lt;/code&gt; callback. More errors are handled and error messages should be more consistent with the actual error (rather than a generic error and an innerException).&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="json-and-rest-service-improvements-in-recent-versions"&gt;Json and REST Service Improvements in recent versions&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;wwJsonSerializer Deserialization Performance Improvements&lt;/strong&gt;&lt;br&gt;
Optimized the .NET parsing of the deserialized object graph for improved performance. Also fixed a few small issues that previously could result in naming conflicts that FoxPro couldn't deal with. Fixed a small issue with UTC dates when &lt;code&gt;AssumeUtcDates&lt;/code&gt; (ie. passthrough as-is dates) is set.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;wwJsonSerializer no longer uses PropertyExclusionList on EMPTY Object&lt;/strong&gt;&lt;br&gt;
When serializing &lt;code&gt;EMPTY&lt;/code&gt; objects, or by association cursors and collections which internally use &lt;code&gt;EMPTY&lt;/code&gt; objects, the &lt;code&gt;PropertyExclusionList&lt;/code&gt; is not applied to properties. The list is meant to keep FoxPro default properties from polluting the output JSON, but EMPTY objects do not have any base properties, so the list is not necessary. This allows for creating properties with reserved FoxPro property names like &lt;code&gt;Comment&lt;/code&gt;, &lt;code&gt;Name&lt;/code&gt;, &lt;code&gt;Classname&lt;/code&gt; etc.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Fix: wwJsonSerializer::AssumeUtcDates Output still converting to Local&lt;/strong&gt;&lt;br&gt;
Fixed issue that when this flag was set, it would not convert the inbound date from local to UTC but use the current date as UTC, but it would still convert the date back to local when deserializing. This change now leaves the deserialized date in the original UTC time, but returns it as a local FoxPro time (ie. the date is not adjusted for timezone) which was the original assumption of this flag. This was broken when we switched from FoxPro based parsing to .NET parsing using JSON.NET. &lt;strong&gt;This is a potentially breaking change if you used this obscure flag in your code&lt;/strong&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;wwJsonServiceClient CallService Url Fix up&lt;/strong&gt;&lt;br&gt;
You can now use a site relative URL by specifying a &lt;code&gt;cServiceBaseUrl&lt;/code&gt; before calling &lt;code&gt;CallService()&lt;/code&gt; which allows you to use site relative paths for the URL. You can use Urls like &lt;code&gt;/authenticate&lt;/code&gt; which makes it easier to switch between different host sites. If the URL does not start with &lt;code&gt;http://&lt;/code&gt; or &lt;code&gt;http://&lt;/code&gt;, the &lt;code&gt;cServiceBaseUrl&lt;/code&gt; is prepended to the URL to create a full URL. This is useful if you switch between different sites such as running against different servers for dev, staging and production servers.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;wwJsonServiceClient: Optionally capture Request and Response Data&lt;/strong&gt;
You can now optionally capture all request and response data via the &lt;code&gt;lSaveRequestData&lt;/code&gt; flag. If set any POSTed JSON data will be capture in &lt;code&gt;cRequestData&lt;/code&gt; and any result data is capture in &lt;code&gt;cResponseData&lt;/code&gt; both of which are useful for debugging.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;wwJsonServiceClient is abstracted into its own PRG File&lt;/strong&gt;&lt;br&gt;
wwJsonServiceClient now has migrated out of the &lt;code&gt;wwJsonSerializer.prg&lt;/code&gt; file to its own &lt;code&gt;wwJsonServiceClient.prg&lt;/code&gt; file. This is a minor breaking change - you'll need to make sure &lt;code&gt;DO wwJsonServiceClient&lt;/code&gt; is called explicitly now to ensure the library is loaded along with all dependencies.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;wwJsonServiceClient CallService Url Fix up&lt;/strong&gt;&lt;br&gt;
You can now use a site relative URL by specifying a &lt;code&gt;cServiceBaseUrl&lt;/code&gt; before calling &lt;code&gt;CallService()&lt;/code&gt; which allows you to use site relative paths for the URL. You can use Urls like &lt;code&gt;/authenticate&lt;/code&gt; which makes it easier to switch between different host sites. If the URL does not start with &lt;code&gt;http://&lt;/code&gt; or &lt;code&gt;http://&lt;/code&gt;, the &lt;code&gt;cServiceBaseUrl&lt;/code&gt; is prepended to the URL to create a full URL. This is useful if you switch between different sites such as running against different servers for dev, staging and production servers.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Fix: wwJsonService UTF-8 Encoding/Decoding&lt;/strong&gt;&lt;br&gt;
Fixed inconsistencies in UTF-8 encoding by the service client. Now data sent is encoded and data received is decoded. Optional parameters allow disabling this auto en/decoding.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="wwcache-improvements"&gt;wwCache Improvements&lt;/h2&gt;
&lt;p&gt;wwCache is an old component in Web Connection that is internally used to cache certain bits of information in a local cursor. It's a great way to cache generated output or any string based value that you don't want to repeatedly regenerate or calculate out.&lt;/p&gt;
&lt;p&gt;The class gains a few common method that were previously missing:  &lt;code&gt;Clear()&lt;/code&gt; that clears the cache and closes the underlying cache cursor to avoid excessive memo bloat and &lt;code&gt;GetOrAddItem()&lt;/code&gt; that combines retrieving an existing value, or setting a new one into the cache in one step.&lt;/p&gt;
&lt;p&gt;Note that in Web Connection the cache object is always available as &lt;code&gt;Server.oCache&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;PRIVATE pcToc

*** Retrieve a cached TOC or generate one
pcToc = Server.oCache.GetOrAddItem(&amp;quot;Toc&amp;quot;,GenerateToc(),3600)

*** pcToc can now be embedded into the template
Response.ExpandTemplate(&amp;quot;~\toc.wcs&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="summary"&gt;Summary&lt;/h2&gt;
&lt;p&gt;Overall this major version release has no  groundbreaking new features, but there are a number of significant and useful enhancements. I think the COM server features in particular are going to be very useful to those of you running busy sites on Web Connection.&lt;/p&gt;
&lt;p&gt;As we go forward Web Connection will continue to do incremental updates of features and roll them into minor release updates, rather than providing big bang new versions with massive amount of changes that few will use due to feature overload ??.&lt;/p&gt;
&lt;p&gt;As always with new releases, please, please report any issues you encounter on the &lt;a href="https://support.west-wind.com"&gt;message board&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Aloha,&lt;/p&gt;
&lt;p&gt;+++ Rick ---&lt;/p&gt;
</description>
     </item>
     <item>
			<title>West Wind Client Tools 8.0 Release Notes</title>
			<pubDate>Thu, 30 May 2024 20:19:04 GMT</pubDate>
			<guid isPermaLink="false">9177_20240530</guid>
			<link>https://west-wind.com/wconnect/weblog/ShowEntry.blog?id=9177</link>
			<dc:creator>Rick Strahl</dc:creator>
			<comments>https://west-wind.com/wconnect/weblog/ShowEntry.blog?id=9177#Feedback</comments>
			<slash:comments>0</slash:comments>
			<description>&lt;p&gt;We've released v8.0 of the West Wind Client Tools. Although this is a major version update, this is not a huge release although there are a few noteworthy changes in the libraries that may require attention when upgrading. It's been 6+ years since the last release and there have been many improvements since so this release can be thought of as a version rollup release more than anything.&lt;/p&gt;
&lt;p&gt;There are a few new features in this update however:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Refactored FTP Support for FTP, FTPS (FTP over TLS) and SFTP&lt;/li&gt;
&lt;li&gt;New ZipFolder() functionality&lt;/li&gt;
&lt;li&gt;Many improvements to wwDotnetBridge&lt;/li&gt;
&lt;li&gt;Many functional and performance improvements in wwJsonSerializer&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="upgrading"&gt;Upgrading&lt;/h2&gt;
&lt;p&gt;This is a major version upgrade, so this is a paid upgrade.&lt;/p&gt;
&lt;p&gt;You can upgrade in the store:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://store.west-wind.com/product/wwClient80_up"&gt;West Wind Client Tools Upgrade&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://store.west-wind.com/product/wwClient80"&gt;West Wind Client Tools&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;If you purchased v7 on or after June 1st, 2023 (a year ago) you can upgrade for free until the end of the year, otherwise an upgrade is required for the new version (details on the upgrade link).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="refactored-ftp-support"&gt;Refactored FTP Support&lt;/h2&gt;
&lt;p&gt;The FTP support in Web Connection and the Client Tools has been pretty creaky for years. The original FTP support was built ontop of the built-in Windows WinINET services. Unfortunately those services do not support secure FTP communication so we've always lacked support for FTPS (FTP over TLS). Some years ago I added the wwSFtp class to provide support SFtp (FTP over SSH) which mitigated some of the deficiencies, but FTPS tends to be pretty common as some popular servers like Filezilla Server use FTPS.&lt;/p&gt;
&lt;p&gt;Long story short, in order to support FTPS I added a new &lt;code&gt;wwFtpClient&lt;/code&gt; class that supports both plain FTP and FTPS in a more reliable manner. This new implementation is built ontop of a popular .NET FTP library that is more accessible, considerably faster, provides for logging and provides much better error handling in case of failures. The new &lt;code&gt;wwFtpClient&lt;/code&gt; is considerably simpler than the old &lt;code&gt;wwFTP&lt;/code&gt; class as it does away with all the WinINET related baggage. As such the model for &lt;code&gt;wwFtpClient&lt;/code&gt; is simply:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Connect()&lt;/li&gt;
&lt;li&gt;Run Ftp Commands&lt;/li&gt;
&lt;li&gt;Close()&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;and those are the only interfaces supported. You can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Download&lt;/li&gt;
&lt;li&gt;Upload&lt;/li&gt;
&lt;li&gt;DeleteFile&lt;/li&gt;
&lt;li&gt;MoveFile&lt;/li&gt;
&lt;li&gt;ListFiles&lt;/li&gt;
&lt;li&gt;ChangeDirectory&lt;/li&gt;
&lt;li&gt;CreateDirectory&lt;/li&gt;
&lt;li&gt;RemoveDirectory&lt;/li&gt;
&lt;li&gt;ExecuteCommand&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The methods are simple and easier to use.&lt;/p&gt;
&lt;p&gt;Along the same vein I've also replaced the &lt;code&gt;wwSFTP&lt;/code&gt; class with &lt;code&gt;wwSFtpClient&lt;/code&gt; which uses the exact same interface as &lt;code&gt;wwFtpClient&lt;/code&gt; so both classes can be used nearly interchangeably. There some small differences in how connections can be assigned but otherwise both classes operate identically - the old classes were similar but not identical. The &lt;code&gt;wwSFtpClient&lt;/code&gt; class uses the same SSH.NET .NET library as before, although it's been rev'd to the latest version.&lt;/p&gt;
&lt;p&gt;The old &lt;code&gt;wwFtp&lt;/code&gt; and &lt;code&gt;wwSFtp&lt;/code&gt; classes are still available in the &lt;code&gt;OldFiles&lt;/code&gt; folder and they continue to work, but the recommendation is to update to the new classes if possible as they are easier to use and more reliable with more supported types of connections (for wwFtpClient).&lt;/p&gt;
&lt;h2 id="zipfolder-and-unzipfolder"&gt;ZipFolder() and UnzipFolder()&lt;/h2&gt;
&lt;p&gt;ZipFolder() is a new library function in &lt;code&gt;wwAPI&lt;/code&gt; that provides zipping functionality from a folder using the built in Windows zip services, meaning there are no external dependencies on additional libraries. These functions use .NET to handle the Zipping interface, which removes the dependency on the old DynaZip dlls.&lt;/p&gt;
&lt;p&gt;The old &lt;code&gt;ZipFiles()&lt;/code&gt; and &lt;code&gt;UnZipFiles()&lt;/code&gt; functions are still available, but they continue to require the dzip/dunzip/zlib1 dlls.&lt;/p&gt;
&lt;h2 id="wwdotnet-bridge-improvements"&gt;wwDotnet Bridge Improvements&lt;/h2&gt;
&lt;p&gt;The last few release of wwDotnetBridge have seen a number of improvements on how result values are passed back to .NET fixing up more types so that they work in .NET. There has been a lot of work around Collection access related to the &lt;code&gt;ComArray&lt;/code&gt; class that is returned for lists and collections. The new &lt;code&gt;AddItem()&lt;/code&gt; method makes it easy to add items to a collection effectively and &lt;code&gt;AddDictionaryItem()&lt;/code&gt; makes it easy to added to key and value  collections. There are also some improvements on how ComArray .NET instances are managed and can be accessed that results in additional use cases that did not previously work in some scenarios.&lt;/p&gt;
&lt;p&gt;Additionally there's been some work to clean up how exceptions are handled and returned in error messages which should result in cleaner error messages without COM artifacts gunking up the message text. We've also fixed exception handling for Task Async operations - exceptions are now returned from failed task operations, rather than the generic failure that was returned prior.&lt;/p&gt;
&lt;h3 id="json-and-rest-service-calls"&gt;JSON and REST Service Calls&lt;/h3&gt;
&lt;p&gt;The wwJsonSerializer is probably one of the more popular Client Tools components and it's getting a few new features in this release. There have been many tweaks in the last couple releases. One has been to optimize &lt;code&gt;EMPTY&lt;/code&gt; object serialization by skipping the exclusion list which is meant to prevent FoxPro base properties from rendering which can improve performance significantly on large lists. There are now options for how dates are handled either as local or UTC dates based on the incoming data's date formatting.&lt;/p&gt;
&lt;p&gt;The JsonSerivceClient gets the ability to capture request and response data optionally which can be useful for debugging or logging/auditing.&lt;/p&gt;
&lt;h2 id="breaking-changes-no-except-for-ftp"&gt;Breaking Changes? No Except for FTP&lt;/h2&gt;
&lt;p&gt;Although this is a major version update, there are no breaking changes other than the new FTP classes. And for those you can continue to use the older &lt;code&gt;wwFtp&lt;/code&gt; and &lt;code&gt;wwSFtp&lt;/code&gt; classes if necessary.&lt;/p&gt;
&lt;p&gt;You will have to make sure to update your DLL dependencies however, so make sure you update:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;wwipstuff.dll&lt;/li&gt;
&lt;li&gt;wwDotnetBridge.dll&lt;/li&gt;
&lt;li&gt;Newtonsoft.Json.dll&lt;/li&gt;
&lt;li&gt;FluentFtp.dll   (for wwFtpClient)&lt;/li&gt;
&lt;li&gt;Renci.SshNet.dll  (for wwSFtpClient)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;+++ Rick ---&lt;/p&gt;
</description>
     </item>
     <item>
			<title>wwDotnetBridge and Loading Native Dependencies for .NET Assemblies</title>
			<pubDate>Sat, 23 Mar 2024 23:12:11 GMT</pubDate>
			<guid isPermaLink="false">9176_20240323</guid>
			<link>https://west-wind.com/wconnect/weblog/ShowEntry.blog?id=9176</link>
			<dc:creator>Rick Strahl</dc:creator>
			<comments>https://west-wind.com/wconnect/weblog/ShowEntry.blog?id=9176#Feedback</comments>
			<slash:comments>0</slash:comments>
			<description>&lt;p&gt;&lt;img src="https://west-wind.com/wconnect/weblog/imageContent/2025/wwDotnetBridge-and-Loading-Native-Dependencies-for-.NET-Assemblies/Loading-Banner.png" alt="Loading Banner"&gt;&lt;/p&gt;
&lt;p&gt;When using wwDotnetBridge to access .NET components you may run into .NET components that have native binary dependencies. When I say 'native' here I mean, non-.NET Win32/C++ dependencies that are generally not following the same assembly loading rules as the .NET host.&lt;/p&gt;
&lt;p&gt;##AD##&lt;/p&gt;
&lt;h2 id="an-example-nhunspell-spellchecking-and-word-suggestions"&gt;An Example: NHunspell, Spellchecking and Word Suggestions&lt;/h2&gt;
&lt;p&gt;Just for context, this is the &lt;code&gt;NHunspell&lt;/code&gt; Spell checking component I highlighted in a &lt;a href="https://bitbucket.org/RickStrahl/swfox16_wwdotnetbridge_10uses/raw/master/Documents/Strahl_wwdotnetBridge_10Uses.pdf"&gt;Southwest FoxSession all the way back in 2016&lt;/a&gt;. This library is based on the popular HUnspell native library, for which NHUnspell is basically a .NET wrapper. The .NET wrapper just makes the appropriate interop calls to native C++ library.&lt;/p&gt;
&lt;p&gt;I created a FoxPro class around the .NET component using &lt;a href="https://github.com/RickStrahl/wwDotnetBridge"&gt;wwDotnetBridge&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;*************************************************************
DEFINE CLASS HunspellChecker AS Custom
*************************************************************
*: Author: Rick Strahl
*:         (c) West Wind Technologies, 2015
*:Contact: http://www.west-wind.com
*:Created: 08/07/15
*************************************************************
oBridge = null
oSpell = null
cLanguage = &amp;quot;en_US&amp;quot;
cDictionaryFolder = &amp;quot;&amp;quot; &amp;amp;&amp;amp; root

************************************************************************
*  init
****************************************
FUNCTION init(lcLang, lcDictFolder)

IF EMPTY(lcLang)
   lcLang = this.cLanguage
ENDIF
IF EMPTY(lcDictFolder)
   lcDictFolder = this.cDictionaryFolder
ENDIF
   
this.oBridge = GetwwDotnetBridge()
IF ISNULL(this.oBridge)
      ERROR &amp;quot;Failed to load HUnspell: &amp;quot; + this.oBridge.cErrorMsg
ENDIF

* ? this.oBridge.GetDotnetVersion()

IF !this.oBridge.LoadAssembly(&amp;quot;NHunspell.dll&amp;quot;)
  ERROR &amp;quot;Failed to load HUnspell: &amp;quot; + this.oBridge.cErrorMsg
ENDIF

IF !EMPTY(lcDictFolder)
	lcDictFolder = ADDBS(lcDictFolder)
ELSE
    lcDictFolder = &amp;quot;&amp;quot;
ENDIF

this.oSpell = this.oBridge.CreateInstance(&amp;quot;NHunspell.Hunspell&amp;quot;,;
                                  lcDictFolder + lcLang + &amp;quot;.aff&amp;quot;,;
                                  lcDictFolder + lcLang + &amp;quot;.dic&amp;quot;)
                                  
IF VARTYPE(this.oSpell) # &amp;quot;O&amp;quot;
   ERROR &amp;quot;Failed to load HUnspell: &amp;quot; + this.oBridge.cErrorMsg
ENDIF  
                                  
IF FILE(lcDictFolder + lcLang + &amp;quot;_custom.txt&amp;quot;)
  lcFile = FILETOSTR(lcDictFolder + lcLang + &amp;quot;_custom.txt&amp;quot;)
  lcFile = STRTRAN(lcFile,CHR(13) + CHR(10),CHR(10))
  lcFile = STRTRAN(lcFile,CHR(13),CHR(10))
  LOCAL ARRAY laLines[1]
  LOCAL lnX, lnLine
  lnLines = ALINES(laLines,lcFile,1 + 4,CHR(10))
  FOR lnX = 1 TO lnLines
      this.oSpell.Add(laLines[lnx])            
  ENDFOR
ENDIF
                                  
IF ISNULL(this.oSpell)
  ERROR &amp;quot;Failed to load HUnspell: &amp;quot; + this.oBridge.cErrorMsg
ENDIF

ENDFUNC
*   init

************************************************************************
*  Spell
****************************************
***  Function: Checks to see if a word is a known word in the dictionary
************************************************************************
FUNCTION Spell(lcWord)

IF ISNULL(lcWord) OR EMPTY(lcWord) OR LEN(lcWord) = 1
   RETURN .T.
ENDIF

RETURN this.oSpell.Spell(lcWord)
ENDFUNC
*   Spell

************************************************************************
*  Suggest
****************************************
***  Function: Gives back a collection of word suggestions for 
***            the passed in word
************************************************************************
FUNCTION Suggest(lcWord)
LOCAL loWords, lnx

loCol = CREATEOBJECT(&amp;quot;collection&amp;quot;)
loWords = this.obridge.InvokeMethod(this.oSpell,&amp;quot;Suggest&amp;quot;,lcWord)
? lowords
? this.oBridge.cErrorMsg

lnCount = this.oBridge.GetProperty(loWords,&amp;quot;Count&amp;quot;)
? this.oBridge.cErrormsg
?  &amp;quot;Count: &amp;quot; + TRANSFORM(lnCount)


FOR lnX = 0 TO lnCount -1
    lcWord =  loWords.Item(lnX)
    loCol.Add( lcWord )
ENDFOR


RETURN loCol
ENDFUNC
*   Suggest


************************************************************************
*  AddWordToDictionary
****************************************
FUNCTION AddWordToDictionary(lcWord, lcLang)

lcFile = &amp;quot;editors\&amp;quot; + lcLang + &amp;quot;_custom.txt&amp;quot;
AppendToFile(lcWord + CHR(13) + CHR(10),lcFile)
this.oSpell.Add(lcWord)

ENDFUNC
*   AddWordToDictionary

************************************************************************
*  Destroy
****************************************
FUNCTION Destroy()

*** MUST dispose to release memory for spell checker
this.oSpell.Dispose()
this.oSpell = null

ENDFUNC
*   Destroy

ENDDEFINE
*EOC HunspellChecker
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To use it you just instantiate the class and pass in a dictionary, then check for spelling or get suggestions:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;loSpell = CREATEOBJECT(&amp;quot;HunspellChecker&amp;quot;,&amp;quot;en_US&amp;quot;,&amp;quot;.\Editors&amp;quot;)   
? loSpell.Spell(&amp;quot;Testing&amp;quot;)
? loSpell.Spell(&amp;quot;Tesdting&amp;quot;)   

lcWord = &amp;quot;aren'tt&amp;quot;
? &amp;quot;Suggest Testding&amp;quot;
loSug = loSpell.Suggest(lcWord)
? loSug.Count
FOR EACH lcWord in loSug
   ? lcWord
ENDFOR
loSpell = null

loSpell = CREATEOBJECT(&amp;quot;HunspellChecker&amp;quot;,&amp;quot;de_DE&amp;quot;,&amp;quot;.\Editors&amp;quot;)
? loSpell.Spell(&amp;quot;Zahn&amp;quot;)
? loSpell.Spell(&amp;quot;ZÃ¤hne&amp;quot;) 
? loSpell.Spell(&amp;quot;lÃ¤uft&amp;quot;) 
loSug = loSpell.Suggest(&amp;quot;ZÃ¤hjne&amp;quot;)
FOR EACH lcWord in loSug
   ? lcWord
ENDFOR
      
? loSug.Count
loSpell = null
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I use this library both in &lt;a href="https://markdownmonster.west-wind.com"&gt;Markdown Monster&lt;/a&gt; (in .NET) and &lt;a href="https://helpbuilder.west-wind.com"&gt;Html Help Builder&lt;/a&gt; (FoxPro) to provide on the fly spell checkiing inside of Markdown documents. The above methods provide both for the error highligghting as well as the suggestions that are popped up in the editor.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://west-wind.com/wconnect/weblog/imageContent/2025/wwDotnetBridge-and-Loading-Native-Dependencies-for-.NET-Assemblies/SpellCheckingInAction.png" alt="Spell Checking In Action"&gt;&lt;/p&gt;
&lt;p&gt;Surprisingly, this is very fast even accounting for interop between JavaScript and FoxPro and FoxPro to .NET!&lt;/p&gt;
&lt;p&gt;This is just to give you some context of a library that has a native dependency (and because I know some of you would ask about the functionality ??)&lt;/p&gt;
&lt;h2 id="library-and-dependencies-watch-load-location"&gt;Library and Dependencies: Watch Load Location&lt;/h2&gt;
&lt;p&gt;So NHunspell has a .NET assembly &lt;code&gt;NHUnspell.dll&lt;/code&gt;  and a &lt;strong&gt;native dependency&lt;/strong&gt; for the underlying Win32 32 bit &lt;code&gt;hunspellx86.dll&lt;/code&gt; (or &lt;code&gt;hunspell.dll&lt;/code&gt; for the 64bit version).&lt;/p&gt;
&lt;p&gt;So the two file dependencies for the .NET component are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;NHunspell.dll (.NET assembly)&lt;/li&gt;
&lt;li&gt;HUnspellx86.dll (Win32 native dll)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="standardnet-assembly-loading-behavior"&gt;Standard.NET Assembly Loading Behavior&lt;/h3&gt;
&lt;p&gt;Natively .NET dependencies resolve only out of the EXE's startup path or an optional &lt;code&gt;PrivateBin&lt;/code&gt; path that can be assigned in &lt;code&gt;app.config&lt;/code&gt; or through the hosting runtime instantiation.&lt;/p&gt;
&lt;p&gt;For a FoxPro application that would mean 1 of 2 things:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Your EXE's startup folder when running the EXE&lt;/li&gt;
&lt;li&gt;VFP9.exe's startup folder&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="wwdotnetbridge-assembly-loading-behavior"&gt;wwDotnetBridge Assembly Loading Behavior&lt;/h3&gt;
&lt;p&gt;wwDotnetBridge tries to be a little more helpful and basically tries to resolve assembly paths for you &lt;strong&gt;if you explicitly load assemblies via &lt;code&gt;LoadAssembly()&lt;/code&gt;&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;loBridge = GetwwDotnetBridge()

*** Looks in current and FoxPro Path
loBridge.LoadAssembly(&amp;quot;NHUnspell.dll&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you specify a non-pathed DLL name, wwDotnetBridge does &lt;code&gt;FULLPATH(lcAssembly)&lt;/code&gt; on it, which resolves to the current path or anything along the FoxPro path to eventually provide an absolute path to the DLL. .NET by default looks for other assemblies in both the startup path and the location an explicitly loaded assembly is loaded from. IOW, any child dependencies automatically inherit the path of the parent for trying to find assemblies.&lt;/p&gt;
&lt;p&gt;Alternately you can also specify an explicit path directly:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-foxpro"&gt;loBridge.LoadASsembly(&amp;quot;c:\libraries\bin\NHUnspell.dll&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;All this is great and predictable, but... it only applies to .NET assemblies, not any natively loaded assemblies.&lt;/p&gt;
&lt;h3 id="natively-loaded-dependencies"&gt;Natively loaded Dependencies&lt;/h3&gt;
&lt;p&gt;If a .NET component has a native dependency as NHunspell has in the form of &lt;code&gt;hunspellx86.dll&lt;/code&gt; that native DLL either has to be explicitly discovered and found by the .NET component, or it uses the default path resolution.&lt;/p&gt;
&lt;h4 id="default-native-path-resolution"&gt;Default Native Path Resolution&lt;/h4&gt;
&lt;p&gt;The &lt;strong&gt;default path resolution&lt;/strong&gt; for a native dependency is that it is loaded only out of the launching EXE's startup folder.&lt;/p&gt;
&lt;h4 id="custom-native-path-resolution"&gt;Custom Native Path Resolution&lt;/h4&gt;
&lt;p&gt;Some libraries use &lt;strong&gt;custom path resolution&lt;/strong&gt; which set up their own folders and &lt;strong&gt;EXPLICITLY&lt;/strong&gt; load out of that folder. For example, the popular &lt;code&gt;LibGit2Sharp&lt;/code&gt; library uses a custom &lt;code&gt;runtime/win-x86&lt;/code&gt; folder where &lt;code&gt;win-x86&lt;/code&gt; is the platform you are using (x86 for FoxPro 32 bit).&lt;/p&gt;
&lt;p&gt;The best way to see where things are expected is to create a .NET project and install the NuGet package into it, and then see exactly where files are placed. Then use the same 'offset' in your own project: If it dumps it into the root, use the same folder as your EXE, if there's a runtime folder duplicate that structure and put the DLL there.&lt;/p&gt;
&lt;h3 id="nunspell-loading"&gt;NUnspell Loading&lt;/h3&gt;
&lt;p&gt;NHUnspell uses default native path resolution so if you are going to use the functionality both in your deployed application and in the FoxPro IDE you need to put the native &lt;code&gt;hunspellx86.dll&lt;/code&gt; in two places:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Your EXE's startup folder&lt;/li&gt;
&lt;li&gt;VFP9.exe startup folder if you're running in the Fox IDE&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="how-this-can-bite-you"&gt;How this can bite you&lt;/h2&gt;
&lt;p&gt;I'm writing about this because this bit me hard today. I recently set up a new laptop and ended up reinstalling all of my applications including Visual FoxPro. I ran Help Builder which was installed using its installer from Chocolatey and it ran fine. Then I fired it up in the FoxPro IDE and it kept crashing on the spell library with &lt;code&gt;.oSpell&lt;/code&gt; not found when running from within VFP.&lt;/p&gt;
&lt;p&gt;Of course I didn't immediately remember that the spell checking logic has a native dependency so I started trying to track down the error. Only fails at dev time, not at deployed runtime. Weird, huh? Not with what we know now, but 3 years from now???&lt;/p&gt;
&lt;p&gt;The problem is that I reinstalled VFP 9 and of course &lt;strong&gt;did not remember to install the &lt;code&gt;hunspellx86.dll&lt;/code&gt;&lt;/strong&gt; in the VFP IDE folder. Once I moved the DLL into the VFP9 install folder all is well.&lt;/p&gt;
&lt;p&gt;To be clear though - my deployed application that runs the EXE still worked because the installer copies the &lt;code&gt;hunspellx86.dll&lt;/code&gt; into the install directory - no issue there, but the VFP install is tricky because you're not going to see anything wrong until you run your specialized application a long time after installation.&lt;/p&gt;
&lt;p&gt;Make notes, and hopefully you'll remember to read them... I will and probably won't remember. Ha!&lt;/p&gt;
&lt;p&gt;##AD##&lt;/p&gt;
&lt;h2 id="summary"&gt;Summary&lt;/h2&gt;
&lt;p&gt;The jist of this post is that when you use a library with native dependencies make sure you understand the native libraries have different load behavior than the .NET libraries, with the default behavior being that it will load out of the startup EXE's startup folder. And remember that for FoxPro applications you typically have two root EXEs that you need to worry about for development in the IDE using VFP9.exe and for development with your own compiled EXE.&lt;/p&gt;
</description>
     </item>
</channel>
</rss>