From f5270d97b13df1d26ffac52e98463eda31b6213d Mon Sep 17 00:00:00 2001 From: Klavs Madsen Date: Sat, 21 Jan 2017 13:34:32 +0100 Subject: [PATCH 1/2] Added support for running C# code in a dotnet core environment --- environments/dotnet/Dockerfile | 9 ++ environments/dotnet/ExecutorModule.cs | 65 ++++++++++ environments/dotnet/FissionCompiler.cs | 59 +++++++++ environments/dotnet/Function.cs | 21 ++++ environments/dotnet/Program.cs | 30 +++++ environments/dotnet/README.md | 158 +++++++++++++++++++++++++ environments/dotnet/build.sh | 3 + environments/dotnet/project.json | 24 ++++ 8 files changed, 369 insertions(+) create mode 100644 environments/dotnet/Dockerfile create mode 100644 environments/dotnet/ExecutorModule.cs create mode 100644 environments/dotnet/FissionCompiler.cs create mode 100644 environments/dotnet/Function.cs create mode 100755 environments/dotnet/Program.cs create mode 100644 environments/dotnet/README.md create mode 100755 environments/dotnet/build.sh create mode 100755 environments/dotnet/project.json diff --git a/environments/dotnet/Dockerfile b/environments/dotnet/Dockerfile new file mode 100644 index 00000000..97582361 --- /dev/null +++ b/environments/dotnet/Dockerfile @@ -0,0 +1,9 @@ +FROM microsoft/dotnet:1.1.0-runtime + +WORKDIR /fission-workdir +COPY out . +EXPOSE 8888 + +ENTRYPOINT ["dotnet"] + +CMD ["fission-dotnet.dll"] \ No newline at end of file diff --git a/environments/dotnet/ExecutorModule.cs b/environments/dotnet/ExecutorModule.cs new file mode 100644 index 00000000..bb90cade --- /dev/null +++ b/environments/dotnet/ExecutorModule.cs @@ -0,0 +1,65 @@ +using Fission.DotNetCore.Compiler; +using System.Collections.Generic; +using Nancy; +using System.IO; +using System; + +namespace Fission.DotNetCore +{ + public class ExecutorModule : NancyModule + { + private const string CODE_PATH = "/userfunc/user"; + + private static Function _userFunc; + + public ExecutorModule() + { + Post("/specialize", args => Specialize()); + Get("/", _ => Run()); + Post("/", _ => Run()); + Put("/", _ => Run()); + Head("/", _ => Run()); + Options("/", _ => Run()); + Delete("/", _ => Run()); + + } + + private object Specialize() + { + var errors = new List(); + if (File.Exists(CODE_PATH)) + { + var code = File.ReadAllText(CODE_PATH); + _userFunc = FissionCompiler.Compile(code, out errors); + if (_userFunc == null) + { + var errstr = string.Join(Environment.NewLine, errors); + Console.WriteLine(errstr); + var response = (Response)errstr; + response.StatusCode = HttpStatusCode.InternalServerError; + return response; + + } + return null; + } + else + { + var response = (Response)"Unable to locate code"; + response.StatusCode = HttpStatusCode.InternalServerError; + return response; + } + } + + private object Run() + { + if (_userFunc == null) + { + var response = (Response)"Generic container: no requests supported"; + response.StatusCode = HttpStatusCode.InternalServerError; + return response; + } + var args = ((DynamicDictionary)Request.Query).ToDictionary(); + return _userFunc.Invoke(args); + } + } +} diff --git a/environments/dotnet/FissionCompiler.cs b/environments/dotnet/FissionCompiler.cs new file mode 100644 index 00000000..495a3ae1 --- /dev/null +++ b/environments/dotnet/FissionCompiler.cs @@ -0,0 +1,59 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.Loader; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Emit; + +namespace Fission.DotNetCore.Compiler +{ + //adapted from this article http://www.tugberkugurlu.com/archive/compiling-c-sharp-code-into-memory-and-executing-it-with-roslyn + class FissionCompiler + { + public static Function Compile(string code, out List errors) { + errors = new List(); + + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(code); + + string assemblyName = Path.GetRandomFileName(); + MetadataReference[] references = new MetadataReference[] + { + MetadataReference.CreateFromFile(typeof(object).GetTypeInfo().Assembly.Location) + }; + CSharpCompilation compilation = CSharpCompilation.Create( + assemblyName, + syntaxTrees: new[] { syntaxTree }, + references: references, + options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + + using (var ms = new MemoryStream()) + { + EmitResult result = compilation.Emit(ms); + + if (!result.Success) + { + IEnumerable failures = result.Diagnostics.Where(diagnostic => + diagnostic.IsWarningAsError || + diagnostic.Severity == DiagnosticSeverity.Error); + + foreach (Diagnostic diagnostic in failures) + { + errors.Add($"{diagnostic.Id}: {diagnostic.GetMessage()}"); + } + } + else + { + ms.Seek(0, SeekOrigin.Begin); + + Assembly assembly = AssemblyLoadContext.Default.LoadFromStream(ms); + var type = assembly.GetType("Fission"); + var info = type.GetMember("Run").First() as MethodInfo; + return new Function(info); + } + } + return null; + } + } +} \ No newline at end of file diff --git a/environments/dotnet/Function.cs b/environments/dotnet/Function.cs new file mode 100644 index 00000000..f60f6928 --- /dev/null +++ b/environments/dotnet/Function.cs @@ -0,0 +1,21 @@ +using System; +using System.Reflection; +using System.Collections.Generic; + +namespace Fission.DotNetCore.Compiler +{ + class Function + { + private readonly MethodInfo _info; + public Function(MethodInfo info) + { + if (info == null) throw new ArgumentNullException(nameof(info)); + _info = info; + } + + public object Invoke(Dictionary args) + { + return _info.Invoke(null, new[] { args }); + } + } +} \ No newline at end of file diff --git a/environments/dotnet/Program.cs b/environments/dotnet/Program.cs new file mode 100755 index 00000000..9206ddf5 --- /dev/null +++ b/environments/dotnet/Program.cs @@ -0,0 +1,30 @@ +using System.IO; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Nancy.Owin; + +namespace Fission.DotNetCore +{ + public class Program + { + public static void Main(string[] args) + { + var host = new WebHostBuilder() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseKestrel() + .UseUrls("http://*:8888") + .UseStartup() + .Build(); + host.Run(); + } + + + } + public class Startup + { + public void Configure(IApplicationBuilder app) + { + app.UseOwin(x => x.UseNancy()); + } + } +} \ No newline at end of file diff --git a/environments/dotnet/README.md b/environments/dotnet/README.md new file mode 100644 index 00000000..85313a00 --- /dev/null +++ b/environments/dotnet/README.md @@ -0,0 +1,158 @@ +# Fission: dotnet C# Environment + +This is a simple dotnet C# environment for Fission. + +It's a Docker image containing the dotnet 1.1.0 runtime. The image +uses Kestrel with Nancy to host the internal web server and uses +Roslyn to compile the uploaded code. + +The image supports compiling and running code with types defined in +mscorlib and does not at present support other library references. +One workaround for this would be to add the references to this project's +project.json file and rebuild the container. + +The environment works via convention where you create a C# class +called Fission which has a static method named Run taking a single +parameter, a dictionary containing any querystring parameters. + +Example of simplest possible class to be executed: + +``` +using System; +using System.Collections.Generic; + +public class Fission { + public static string Run(Dictionary args) { + return null; + } +} +``` + +Please see examples below. + +## Rebuilding and pushing the image + +To rebuild the image you need either a computer with dotnet 1.1.0 +installed or else you will have to map the source directory into a +container containing the dotnet 1.1.0 environment. + +### Locally installed Dotnet 1.1.0 + +Simply move to the source directory in a terminal and run the ./build.sh script. + +The script will restore dependencies, compile a release build and +and build the container. If you need to change the name of the container +simply change it in the script. + +After the build finishes push the new image to a Docker registry using the +standard procedure. + +### Build in a container + +Move to the directory containing the source and start the Docker container +with dotnet and mount the current directory to a build location: + +``` +docker run -it --rm -v $PWD:/build microsoft/dotnet +``` + +Move to the build directory inside the container and restore the packages: + +``` +cd /build +dotnet restore +log : Restoring packages for /source/project.json... +log : Installing System.Net.WebSockets 4.0.0. +log : Installing runtime.native.System.IO.Compression 4.1.0. +... +``` + +Compile and publish a release build of the source to the 'out' folder: + +``` +dotnet publish -c Release -o out +Publishing source for .NETCoreApp,Version +... +``` +Exit the build container and build the Docker container on the local host: + +``` +exit +docker build -t USER/dotnet-env . +``` +After the build finishes push the new image to a Docker registry using the +standard procedure. + +## Echo example + +### Setup fission environment +First you need to setup the fission according to your cluster setup as +specified here: https://github.com/fission/fission + + +### Create the class to run + +Secondly you need to create a file /tmp/func.cs containing the following code: + +``` +using System; +using System.Collections.Generic; + +public class Fission { + public static string Run(Dictionary args) { + return (string)args["text"]; + } +} +``` +### Run the example + +Lastly to run the example: + +``` +$ fission env create --name dotnet --image fission/dotnet-env + +$ fission function create --name echo --env dotnet --code /tmp/func.cs + +$ fission route create --method GET --url /echo --function echo + +$ curl http://$FISSION_ROUTER/echo?text=hello%20world! + hello world +``` + +## Addition service example + +### Setup fission environment +First you need to setup the fission according to your cluster setup as +specified here: https://github.com/fission/fission + + +### Create the class to run + +Secondly you need to create a file /tmp/func.cs containing the following code: + +``` +using System; +using System.Collections.Generic; + +public class Fission { + public static string Run(Dictionary args) { + var x = Convert.ToInt32(args["x"]); + var y = Convert.ToInt32(args["y"]); + return (x+y).ToString(); + } +} +``` +### Run the example + +Lastly to run the example: + +``` +$ fission env create --name dotnet --image fission/dotnet-env + +$ fission function create --name addition --env dotnet --code /tmp/func.cs + +$ fission route create --method GET --url /add --function addition + +$ curl "http://$FISSION_ROUTER/add?x=30&y=12" + 42 +``` diff --git a/environments/dotnet/build.sh b/environments/dotnet/build.sh new file mode 100755 index 00000000..dc391208 --- /dev/null +++ b/environments/dotnet/build.sh @@ -0,0 +1,3 @@ +dotnet restore +dotnet publish -c Release -o out +docker build -t fission/dotnet-env . \ No newline at end of file diff --git a/environments/dotnet/project.json b/environments/dotnet/project.json new file mode 100755 index 00000000..2c1ea729 --- /dev/null +++ b/environments/dotnet/project.json @@ -0,0 +1,24 @@ +{ + "version": "1.0.0-*", + "buildOptions": { + "emitEntryPoint": true, + "outputName": "fission-dotnet" + }, + + "dependencies": { + "Microsoft.NETCore.App": { + "version": "1.1.0", + "type": "platform" + }, + "Microsoft.AspNetCore.Server.Kestrel": "1.0.0", + "Microsoft.AspNetCore.Owin": "1.0.0", + "Nancy": "2.0.0-barneyrubble", + "Microsoft.CodeAnalysis.CSharp": "1.3.0-*", + "System.Runtime.Loader": "4.0.0-*", + }, + "frameworks": { + "netcoreapp1.0": { + "imports": "dnxcore50" + } + } +} \ No newline at end of file From 760e02114110b6637cfb217b9c2c8e9bdc3f3d6e Mon Sep 17 00:00:00 2001 From: Klavs Madsen Date: Sun, 22 Jan 2017 15:45:08 +0100 Subject: [PATCH 2/2] Added basic logging, just a facade for now Added FissionContext class Changed name on both class and function for input code to avoid namespace clashes when adding FissionContext Updated README Added section on how to develop/debug the code to README, might be obvious for some but not all Changed code input from static function to method --- environments/dotnet/.gitignore | 6 ++ environments/dotnet/ExecutorModule.cs | 27 +++++++-- environments/dotnet/FissionCompiler.cs | 43 ++++++++++----- environments/dotnet/FissionContext.cs | 56 +++++++++++++++++++ environments/dotnet/Function.cs | 17 ++++-- environments/dotnet/Program.cs | 13 +---- environments/dotnet/README.md | 76 ++++++++++++++++++++------ environments/dotnet/build.sh | 2 +- 8 files changed, 186 insertions(+), 54 deletions(-) create mode 100644 environments/dotnet/.gitignore create mode 100644 environments/dotnet/FissionContext.cs diff --git a/environments/dotnet/.gitignore b/environments/dotnet/.gitignore new file mode 100644 index 00000000..8a82e2f6 --- /dev/null +++ b/environments/dotnet/.gitignore @@ -0,0 +1,6 @@ +bin/* +obj/* +out/* +.vscode/* +project.lock* +TODO_dotnet \ No newline at end of file diff --git a/environments/dotnet/ExecutorModule.cs b/environments/dotnet/ExecutorModule.cs index bb90cade..ef1d7fd2 100644 --- a/environments/dotnet/ExecutorModule.cs +++ b/environments/dotnet/ExecutorModule.cs @@ -1,4 +1,5 @@ using Fission.DotNetCore.Compiler; +using Fission.DotNetCore.Api; using System.Collections.Generic; using Nancy; using System.IO; @@ -8,9 +9,13 @@ namespace Fission.DotNetCore { public class ExecutorModule : NancyModule { +#if DEBUG + private const string CODE_PATH = "/tmp/func.cs"; +#else private const string CODE_PATH = "/userfunc/user"; - +#endif private static Function _userFunc; + private static Logger _logger = new Logger(); public ExecutorModule() { @@ -21,7 +26,6 @@ namespace Fission.DotNetCore Head("/", _ => Run()); Options("/", _ => Run()); Delete("/", _ => Run()); - } private object Specialize() @@ -34,7 +38,7 @@ namespace Fission.DotNetCore if (_userFunc == null) { var errstr = string.Join(Environment.NewLine, errors); - Console.WriteLine(errstr); + _logger.WriteError(errstr); var response = (Response)errstr; response.StatusCode = HttpStatusCode.InternalServerError; return response; @@ -44,7 +48,9 @@ namespace Fission.DotNetCore } else { - var response = (Response)"Unable to locate code"; + var errstr = $"Unable to locate code at '{CODE_PATH}'"; + _logger.WriteError(errstr); + var response = (Response)errstr; response.StatusCode = HttpStatusCode.InternalServerError; return response; } @@ -59,7 +65,18 @@ namespace Fission.DotNetCore return response; } var args = ((DynamicDictionary)Request.Query).ToDictionary(); - return _userFunc.Invoke(args); + try + { + return _userFunc.Invoke(new FissionContext(args, new Logger())); + } + catch (Exception e) + { + _logger.WriteError(e.ToString()); + var response = (Response)e.Message; + response.StatusCode = HttpStatusCode.BadRequest; + return response; + } } } } + diff --git a/environments/dotnet/FissionCompiler.cs b/environments/dotnet/FissionCompiler.cs index 495a3ae1..99f236cd 100644 --- a/environments/dotnet/FissionCompiler.cs +++ b/environments/dotnet/FissionCompiler.cs @@ -12,31 +12,46 @@ namespace Fission.DotNetCore.Compiler //adapted from this article http://www.tugberkugurlu.com/archive/compiling-c-sharp-code-into-memory-and-executing-it-with-roslyn class FissionCompiler { - public static Function Compile(string code, out List errors) { + public static Function Compile(string code, out List errors) + { errors = new List(); SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(code); - + string assemblyName = Path.GetRandomFileName(); - MetadataReference[] references = new MetadataReference[] + + var coreDir = Directory.GetParent(typeof(Enumerable).GetTypeInfo().Assembly.Location); + + List references = new List { - MetadataReference.CreateFromFile(typeof(object).GetTypeInfo().Assembly.Location) + MetadataReference.CreateFromFile(coreDir.FullName + Path.DirectorySeparatorChar + "mscorlib.dll"), + MetadataReference.CreateFromFile(typeof(object).GetTypeInfo().Assembly.Location), + MetadataReference.CreateFromFile(Assembly.GetEntryAssembly().Location) }; + + foreach (var referencedAssembly in Assembly.GetEntryAssembly().GetReferencedAssemblies()) + { + var assembly = Assembly.Load(referencedAssembly); + references.Add(MetadataReference.CreateFromFile(assembly.Location)); + } + CSharpCompilation compilation = CSharpCompilation.Create( assemblyName, syntaxTrees: new[] { syntaxTree }, references: references, - options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); - + options: new CSharpCompilationOptions( + OutputKind.DynamicallyLinkedLibrary, + optimizationLevel: OptimizationLevel.Release)); + using (var ms = new MemoryStream()) { EmitResult result = compilation.Emit(ms); if (!result.Success) { - IEnumerable failures = result.Diagnostics.Where(diagnostic => - diagnostic.IsWarningAsError || - diagnostic.Severity == DiagnosticSeverity.Error); + IEnumerable failures = result.Diagnostics.Where(diagnostic => + diagnostic.IsWarningAsError || + diagnostic.Severity == DiagnosticSeverity.Error).ToList(); foreach (Diagnostic diagnostic in failures) { @@ -46,14 +61,14 @@ namespace Fission.DotNetCore.Compiler else { ms.Seek(0, SeekOrigin.Begin); - + Assembly assembly = AssemblyLoadContext.Default.LoadFromStream(ms); - var type = assembly.GetType("Fission"); - var info = type.GetMember("Run").First() as MethodInfo; - return new Function(info); + var type = assembly.GetType("FissionFunction"); + var info = type.GetMember("Execute").First() as MethodInfo; + return new Function(assembly, type, info); } } return null; } } -} \ No newline at end of file +} diff --git a/environments/dotnet/FissionContext.cs b/environments/dotnet/FissionContext.cs new file mode 100644 index 00000000..977444c7 --- /dev/null +++ b/environments/dotnet/FissionContext.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; + +namespace Fission.DotNetCore.Api +{ + public class FissionContext + { + public FissionContext(Dictionary args, Logger logger) + { + if (args == null) throw new ArgumentNullException(nameof(args)); + if (logger == null) throw new ArgumentNullException(nameof(logger)); + Arguments = args; + Logger = logger; + } + + public Dictionary Arguments { get; private set; } + public Logger Logger { get; private set; } + } + + public class Logger + { + public void Write(Severity severity, string format, params object[] args) + { + Console.WriteLine($"{DateTime.Now.ToString("MM/dd/yy H:mm:ss zzz")} {severity}: " + format, args); + } + + public void WriteInfo(string format, params object[] args){ + Write(Severity.Info, format, args); + } + + public void WriteWarning(string format, params object[] args){ + Write(Severity.Warning, format, args); + } + + public void WriteError(string format, params object[] args){ + Write(Severity.Error, format, args); + } + + public void WriteCritical(string format, params object[] args){ + Write(Severity.Critical, format, args); + } + + public void WriteVerbose(string format, params object[] args){ + Write(Severity.Verbose, format, args); + } + } + + public enum Severity + { + Info, + Warning, + Error, + Critical, + Verbose + } +} \ No newline at end of file diff --git a/environments/dotnet/Function.cs b/environments/dotnet/Function.cs index f60f6928..f8b5f724 100644 --- a/environments/dotnet/Function.cs +++ b/environments/dotnet/Function.cs @@ -1,21 +1,28 @@ using System; using System.Reflection; -using System.Collections.Generic; +using Fission.DotNetCore.Api; namespace Fission.DotNetCore.Compiler { class Function { + private readonly Assembly _assembly; + private readonly Type _type; private readonly MethodInfo _info; - public Function(MethodInfo info) + + public Function(Assembly assembly, Type type, MethodInfo info) { if (info == null) throw new ArgumentNullException(nameof(info)); + if (assembly == null) throw new ArgumentNullException(nameof(assembly)); + if (type == null) throw new ArgumentNullException(nameof(type)); + _assembly = assembly; + _type = type; _info = info; } - public object Invoke(Dictionary args) + public object Invoke(FissionContext context) { - return _info.Invoke(null, new[] { args }); + return _info.Invoke(_assembly.CreateInstance(_type.FullName), new[] { context }); } } -} \ No newline at end of file +} diff --git a/environments/dotnet/Program.cs b/environments/dotnet/Program.cs index 9206ddf5..85dc1628 100755 --- a/environments/dotnet/Program.cs +++ b/environments/dotnet/Program.cs @@ -13,18 +13,9 @@ namespace Fission.DotNetCore .UseContentRoot(Directory.GetCurrentDirectory()) .UseKestrel() .UseUrls("http://*:8888") - .UseStartup() + .Configure(app => app.UseOwin(x => x.UseNancy())) .Build(); host.Run(); } - - } - public class Startup - { - public void Configure(IApplicationBuilder app) - { - app.UseOwin(x => x.UseNancy()); - } - } -} \ No newline at end of file +} diff --git a/environments/dotnet/README.md b/environments/dotnet/README.md index 85313a00..9f012558 100644 --- a/environments/dotnet/README.md +++ b/environments/dotnet/README.md @@ -12,19 +12,22 @@ One workaround for this would be to add the references to this project's project.json file and rebuild the container. The environment works via convention where you create a C# class -called Fission which has a static method named Run taking a single -parameter, a dictionary containing any querystring parameters. +called FissionFunction which has a method named Execute taking a single +parameter, a FissionContext object. + +The FissionContext object gives access to the arguments and other items +like logging. Please see FissionContext.cs for public API. Example of simplest possible class to be executed: ``` using System; -using System.Collections.Generic; +using Fission.DotNetCore.Api; -public class Fission { - public static string Run(Dictionary args) { - return null; - } +public class FissionFunction { + public string Execute(FissionContext context) { + return null; + } } ``` @@ -96,12 +99,14 @@ Secondly you need to create a file /tmp/func.cs containing the following code: ``` using System; -using System.Collections.Generic; +using Fission.DotNetCore.Api; -public class Fission { - public static string Run(Dictionary args) { - return (string)args["text"]; - } +public class FissionFunction +{ + public string Execute(FissionContext context){ + context.Logger.WriteInfo("executing.. {0}", context.Arguments["text"]); + return (string)context.Arguments["text"]; + } } ``` ### Run the example @@ -132,14 +137,15 @@ Secondly you need to create a file /tmp/func.cs containing the following code: ``` using System; -using System.Collections.Generic; +using Fission.DotNetCore.Api; -public class Fission { - public static string Run(Dictionary args) { - var x = Convert.ToInt32(args["x"]); - var y = Convert.ToInt32(args["y"]); +public class FissionFunction +{ + public string Execute(FissionContext context){ + var x = Convert.ToInt32(context.Arguments["x"]); + var y = Convert.ToInt32(context.Arguments["y"]); return (x+y).ToString(); - } + } } ``` ### Run the example @@ -156,3 +162,37 @@ $ fission route create --method GET --url /add --function addition $ curl "http://$FISSION_ROUTER/add?x=30&y=12" 42 ``` + +## Developing/debugging the enviroment locally + +The easiest way to debug the environment is to open the directory in +Visual Studio Code (VSCode) as that will setup debugger for you the +first time. + +Remember to install the excellent extension +"C# for Visual Studio Code(powered by OmniSharp)" to get statement completion + +The class ExecutorModule contain preprocessor directive overriding where +the input code file should be found: + +``` +#if DEBUG + private const string CODE_PATH = "/tmp/func.cs"; +#else + private const string CODE_PATH = "/userfunc/user"; +#endif +``` + +So what you need to do is: +1. Open the directory in VSCode. +This will prompt restore of packages and query is debugger setup is needed. Accept both prompts. +2. Press F5 to start the web server. Set breakpoints etc.. +3. Add a code file containing valid C# at /tmp/func.cs +4. Specialize the service with curl via post +``` +$ curl -XPOST http://localhost:8888/specialize +``` +5. Call your function with curl +``` +$ curl -XGET http://localhost:8888 +``` diff --git a/environments/dotnet/build.sh b/environments/dotnet/build.sh index dc391208..b6622ac3 100755 --- a/environments/dotnet/build.sh +++ b/environments/dotnet/build.sh @@ -1,3 +1,3 @@ dotnet restore dotnet publish -c Release -o out -docker build -t fission/dotnet-env . \ No newline at end of file +docker build -t fission/dotnet-env .