Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Running Node.js from .NET using .NET Core #92

Closed
JohnnyFun opened this issue Nov 7, 2019 · 14 comments
Closed

Running Node.js from .NET using .NET Core #92

JohnnyFun opened this issue Nov 7, 2019 · 14 comments

Comments

@JohnnyFun
Copy link

Hi,

I've tried running the following example in .net core 2.0, 2.1, 2.2, and 3.0, but all of them can't seem to recognize the Edgejs dll. It works perfectly in .net framework 4.7.2, with exact same code and nuget reference:

using System;
using System.Threading.Tasks;
using EdgeJs;

class Program
{
    public static async Task Start()
    {
        var func = Edge.Func(@"
            return function (data, callback) {
                callback(null, 'Node.js welcomes ' + data);
            }
        ");

        Console.WriteLine(await func(".NET"));
    }

    static void Main(string[] args)
    {
        Start().Wait();
    }
}

Am I missing something simple, or does edge-js not support .net core calling nodejs from c#?

Any insight would be appreciated. Thanks!

@JohnnyFun
Copy link
Author

For anyone running into same issue, I worked around by simply calling nodejs in a Process from c# and passing data via stdin to nodejs process. Then nodejs talks back via stdout.

@agracio agracio closed this as completed Dec 29, 2020
@tarekahf
Copy link

For anyone running into same issue, I worked around by simply calling nodejs in a Process from c# and passing data via stdin to nodejs process. Then nodejs talks back via stdout.

Could you please give an example?

@JohnnyFun
Copy link
Author

@tarekahf sure:

c# calling node and listening for output/errors:

namespace Features.DynamicNodeJs {
  using System;
  using System.Diagnostics;
  using System.IO;
  using System.Text;
  using System.Text.RegularExpressions;
  using System.Threading.Tasks;
  using CN.Infrastructure.Utilities;

  public class NodeJsService {
    public static async Task<string> RunNodeJsAsync(string data) {
      // call nodejs directly hydrate the webhook body expression
      // edge.js doesn't support .net core yet and it freezes up if you call it from here to a fullframework dll: https://github.com/agracio/edge-js/issues/92. And their INodeServices is basically not unit-testable.
      var result = new StringBuilder();
      var error = new StringBuilder();
      using var proc = new Process {
        StartInfo = new ProcessStartInfo {
          FileName = "node",
          UseShellExecute = false,
          RedirectStandardOutput = true,
          RedirectStandardError = true,
          RedirectStandardInput = true,
          CreateNoWindow = true
        }
      };
      proc.ErrorDataReceived += (s, e) => error.Append('\n').Append(e.Data);
      proc.OutputDataReceived += (s, e) => result.Append('\n').Append(e.Data);
      // For my actual needs, I only passed a json string to the process to use as data.
      // If you really need to run dynamic js, just be careful to not allow injection of malicious code--for instance, code that could sensitive setting data from your server.
      // I just wanted to show how you could achieve the same result as the example code in my request.
      // An alternative to `eval` would be to create a .js file in c# and then run that file, fwiw.
      proc.StartInfo.ArgumentList.Add(Path.Combine(IOUtils.WebAppDir, "ServerJs/run-dynamic-js-script.js"));
      proc.Start();
      proc.BeginOutputReadLine();
      proc.BeginErrorReadLine();
      var messageEnder = "--END-OF-DYNAMIC-JS--"; // nodejs needs to know when it has the full dynamic js script to run.
      await proc.StandardInput.WriteLineAsync(data + messageEnder);

      if (!proc.WaitForExit(5000)) throw new Exception($"Your node script took too long to finish running. Result so far:\n{result}.\n\nError so far:\n{error}");

      // Error message could contain stack trace, so depending on who will see it, you may want to strip that out.
      var errorMsg = error.ToString();
      if (proc.ExitCode != 0 || !string.IsNullOrWhiteSpace(errorMsg)) throw new Exception(errorMsg);
      var rawOutput = result.ToString();

      // if you know your dynamic js script will not console.log, you can simply return rawOutput at this point.
      var resultDelimeter = "--RESULT-DELIMITER--";
      var resultMatch = new Regex($@"{resultDelimeter}\n(.+)\n{resultDelimeter}", RegexOptions.Multiline).Match(rawOutput);
      var dynamicJsResult = resultMatch.Groups[1].Value;
      if (string.IsNullOrWhiteSpace(dynamicJsResult))
        throw new Exception($"body was not empty, but the result was empty. Raw nodejs output: {rawOutput}");
      return dynamicJsResult;
    }
  }
}

run-dynamic-js-script.js (called by the above c# code)

// moment and lodash are available to use in your dynamic js script
const moment = require('./moment.min.js')
const _ = require('./lodash.min.js')

// await template and ctx from stdin
let dynamicJsScript = ''
process.stdin.on('data', d => {
  const line = d.toString()
  dynamicJsScript += line

  // Keep collecting input data til c# says it's done sending data
  const messageEnder = /--END-OF-DYNAMIC-JS--$/m
  console.log('|' + line + '|', messageEnder.test(line))
  if (!messageEnder.test(line)) return
  dynamicJsScript = dynamicJsScript.replace(messageEnder, '')

  // Now that we have the full dynamic js, run it and collect its result.
  const runDynamicJs = `(function(ctx) {
    ${dynamicJsScript}
  })()`
  const result = eval(runDynamicJs)

  // Assume that if the dynamic js returns an object or array, they want JSON back
  const resultSerialized = Array.isArray(result) || typeof result === 'object' ? JSON.stringify(result) : result.toString()

  // Surround output with a delimeter, so that you can still console.log in your dynamic js (which also writes to stdout) without messing up c#'s ability to parse out the result of the dynamic js.
  // If you know that your dynamic js won't console.log, this is unnecessary.
  const resultDelimeter = '--RESULT-DELIMITER--'
  process.stdout.write(`${resultDelimeter}\n${resultSerialized}\n${resultDelimeter}`)
  process.exit()
})

c# unit test:

namespace Tests.Features.NodeJs {
  using System.Threading.Tasks;
  using global::Features.DynamicNodeJs;
  using Xunit;

  public class NodeJsServiceTests {
    [Fact]
    public async Task RunNodeJsTest() {
      var result = await NodeJsService.RunNodeJsAsync(@"
        return {
          momentWorks: moment('2022-03-27').add(1, 'day').format('YYYY-MM-DD'),
          lodashWorks: _.upperCase('hello'),
        }
      ");
      Assert.Equal("{\"momentWorks\":\"2022-03-28\",\"lodashWorks\":\"HELLO\"}", result);
    }
  }
}

@tarekahf
Copy link

@JohnnyFun Thank you so much!

In the meantime, did you figure out why the following is not recognized by vs code on a C# project?

using EdgeJs;
...

I get error type not found or something like that?
I used NuGet to add Edge.js package.

Could you please do me a favour and create a .NET 5.0 console project in VS code and wtite the simplest program to call nodejs from C#?

I appreciate your help.

Tarek

@JohnnyFun
Copy link
Author

Oh I should've done that right out the gate. Here ya go: https://github.com/JohnnyFun/NodeJsFromCSharp

@tarekahf
Copy link

Oh I should've done that right out the gate. Here ya go: https://github.com/JohnnyFun/NodeJsFromCSharp

This is fantastic! I just changed the target .NET to NET5.0 and all worked like a charm. This will work.

I was wondering why the original method is not working? You are not using Edge.js Package correct?

Why I am not able t use Edge.js package in a regular C# .NET Console Project?

Are you able to use Edge.js Package in a normal C# .NET Console Project in vs code?

I appreciate your help.

Tarek

@JohnnyFun
Copy link
Author

Cool, I'm running [email protected] fwiw, so maybe that's why it worked for me. Glad you got it working though!

My console app doesn't use Edge.js. It has no nuget dependencies. I'm not sure why this issue was closed--perhaps it works now? I didn't check, since my nodejs workaround works for my needs 🤷

@tarekahf
Copy link

Well, I created another issue in this GitHub, and if they don't reply, I will use your method. I a not sure what is the value of Edge.js in this case? Your method replaced the whole framework! It is not worth it to spend days trying to figure out why the using EdgeJs; statement in C# is not working from VS Code Project!!!!

My plan is to use C# Project to Automate Unit Testing and QA End-to-End testing using Selenium and xUnit. The problem is that we need to invoke a number of complex APIs that are already set up under Postman, and it would be a pain to program such calls in C#. I figured out that I can use Newman NodeJs in this case, and integrate it with C#. We can invoke the Postman collections using NodeJs Newman, and in turn invoke the Newman calls from C#, in addition to using Selenium to control the Browser.

So the C# Project will perform the following:

  • Invoke Newman collection from C# using NodeJs to create the test case.
  • Invoke Selenium APIs from C# to control the Browser and carry out the testing.

I thought to share this with you in case you have any feedback.

I appreciate your help.

Regards,
Tarek

@tarekahf
Copy link

@JohnnyFun I figured out what was wrong. According to the Edge.js package description, the framework net50 is not supported. I change the target framework to net48 and it worked. Now I have two options... thank you so much.

@JohnnyFun
Copy link
Author

You could just invoke your postman tests from the CLI as part of your CI pipeline. Unless I'm missing something, your needs don't require invoking dynamic js from c#.

@tarekahf
Copy link

But how I can assert the result of the Newman invocation when the Selenium project kicks in? In Jenkins pipeline, I'll invoke the Newman command as a command line. I won't have an easy way (at least as per my findings) to confirm if the call is successful, get the post response details, and pass it on to the Selenium program to continue the test process.

I know I can find work arounds, but I imagine it's going to be a pain to do.

Besides, I need to give this setup to the QA team for end to end testing. At certain times, it's not possible to setup Jenkins pipeline only for end to end testing. So, best is to setup vs code project for such end to end testing automation. I though this is one of the justified use cases for using Edge.js.

The other option is to use Node js for Selenium project which I haven't researched yet.

If you can direct me to links or examples to help in this or if you have feedback, it'll so thankful for you.

@JohnnyFun
Copy link
Author

Sure, you know more about your situation than me. All that stuff gets pretty out of the scope of this humble github issue, so I'll just say good luck and godspeed, sir!

@tarekahf
Copy link

Can you tell me if in your project sample, will you be able to pass on parameters to NodeJS program, execute Newman NodeJs call, and get the response back?

The Newman invocation is received in a callback. I didn't do a deep-dive in your project, but couldn't figure out if I can use a callback to send the result back from NodeJS to C#.

Sorry as I asked you many questions...

Tarek

@JohnnyFun
Copy link
Author

Yeah, just build the nodejs string of code differently, bake your data into the code that'll get eval'd. Checkout Program.cs

@agracio agracio changed the title The type or namespace 'Edgejs' could not be found (.net core) Running Node.js from .NET using .NET Core Dec 21, 2024
@agracio agracio pinned this issue Dec 21, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants