diff --git a/dotnet8/Dockerfile b/dotnet8/Dockerfile new file mode 100644 index 00000000..0704f190 --- /dev/null +++ b/dotnet8/Dockerfile @@ -0,0 +1,18 @@ +# Use the ASP.NET Core 8 base image +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base +WORKDIR /app + +# Use the .NET 8 SDK image for the build +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +WORKDIR /src +COPY . . + +RUN dotnet restore Fission.DotNet/Fission.DotNet.csproj + +RUN dotnet publish Fission.DotNet/Fission.DotNet.csproj -c Release -o /app + +# Final image +FROM base AS final +WORKDIR /app +COPY --from=build /app . +ENTRYPOINT ["dotnet", "Fission.DotNet.dll"] diff --git a/dotnet8/Fission.DotNet.Common/Fission.DotNet.Common.csproj b/dotnet8/Fission.DotNet.Common/Fission.DotNet.Common.csproj new file mode 100644 index 00000000..0ffdcef5 --- /dev/null +++ b/dotnet8/Fission.DotNet.Common/Fission.DotNet.Common.csproj @@ -0,0 +1,26 @@ + + + + net8.0 + enable + enable + + + + Fission.DotNet.Common + 1.1.0 + Lorenzo Caldon + Fission dotnet environment common package + GPL-3.0-or-later + https://github.com/lcsoft77/fission-env-dotnet8 + ./nupkg + + + + README.md + + + + + + diff --git a/dotnet8/Fission.DotNet.Common/Fission.DotNet.Common.sln b/dotnet8/Fission.DotNet.Common/Fission.DotNet.Common.sln new file mode 100644 index 00000000..7ea97c8d --- /dev/null +++ b/dotnet8/Fission.DotNet.Common/Fission.DotNet.Common.sln @@ -0,0 +1,24 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.2.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Fission.DotNet.Common", "Fission.DotNet.Common.csproj", "{66CA3524-4798-6315-18DF-47CD0B936395}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {66CA3524-4798-6315-18DF-47CD0B936395}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {66CA3524-4798-6315-18DF-47CD0B936395}.Debug|Any CPU.Build.0 = Debug|Any CPU + {66CA3524-4798-6315-18DF-47CD0B936395}.Release|Any CPU.ActiveCfg = Release|Any CPU + {66CA3524-4798-6315-18DF-47CD0B936395}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {AF6A70F1-84DB-4983-A581-08BC54DF98A5} + EndGlobalSection +EndGlobal diff --git a/dotnet8/Fission.DotNet.Common/FissionContext.cs b/dotnet8/Fission.DotNet.Common/FissionContext.cs new file mode 100644 index 00000000..7ffb1512 --- /dev/null +++ b/dotnet8/Fission.DotNet.Common/FissionContext.cs @@ -0,0 +1,72 @@ +using System.Text; +using System.Text.Json; + +namespace Fission.DotNet.Common; + +public class FissionContext +{ + protected Stream _content; + protected Dictionary _arguments; + protected Dictionary _headers; + protected Dictionary _parameters; + + public FissionContext(Stream body, Dictionary arguments, Dictionary headers, Dictionary parameters) + { + if (body == null) throw new ArgumentNullException(nameof(body)); + if (arguments == null) throw new ArgumentNullException(nameof(arguments)); + if (headers == null) throw new ArgumentNullException(nameof(headers)); + if (parameters == null) throw new ArgumentNullException(nameof(parameters)); + + _content = body; + _arguments = arguments; + _headers = headers; + _parameters = parameters; + } + + protected string GetHeaderValue(string key, string defaultValue = null) + { + return _headers.ContainsKey(key) ? _headers[key] : defaultValue; + } + + public Dictionary Arguments => _arguments; + public Dictionary Parameters => _parameters; + + public string TraceID => GetHeaderValue("traceparent", Guid.NewGuid().ToString()); + public string FunctionName => GetHeaderValue("X-Fission-Function-Name"); + public string Namespace => GetHeaderValue("X-Fission-Function-Namespace"); + public string ResourceVersion => GetHeaderValue("X-Fission-Function-Resourceversion"); + public string UID => GetHeaderValue("X-Fission-Function-Uid"); + public string Trigger => GetHeaderValue("Source-Name"); + public string ContentType => GetHeaderValue("Content-Type"); + public Int32 ContentLength => GetHeaderValue("Content-Length") != null ? Int32.Parse(GetHeaderValue("Content-Length")) : 0; + public Stream Content => _content; + + public async Task ContentAsString() + { + if (_content == null) + { + return null; + } + + _content.Position = 0; + using (StreamReader reader = new StreamReader(_content, Encoding.UTF8, leaveOpen: true)) + { + return await reader.ReadToEndAsync(); + } + } + + public async Task ContentAs(JsonSerializerOptions? options = null) + { + if (_content == null) + { + return default; + } + + _content.Position = 0; + using (StreamReader reader = new StreamReader(_content, Encoding.UTF8, leaveOpen: true)) + { + string content = await reader.ReadToEndAsync(); + return JsonSerializer.Deserialize(content, options); + } + } +} diff --git a/dotnet8/Fission.DotNet.Common/FissionHttpContext.cs b/dotnet8/Fission.DotNet.Common/FissionHttpContext.cs new file mode 100644 index 00000000..08ef8ec8 --- /dev/null +++ b/dotnet8/Fission.DotNet.Common/FissionHttpContext.cs @@ -0,0 +1,42 @@ +using System; +using System.Text; +using System.Text.Json; + +namespace Fission.DotNet.Common; + +public class FissionHttpContext : FissionContext +{ + private string _method; + + public FissionHttpContext(Stream body, string method, Dictionary arguments, Dictionary headers, Dictionary parameters) : base(body, arguments, headers, parameters) + { + _method = method; + } + + public Dictionary Headers => _headers; + public string Url + { + get + { + var urlHeader = GetHeaderValue("X-Fission-Full-Url"); + + if (urlHeader != null) + { + if (urlHeader.Contains("?")) + { + urlHeader = urlHeader.Substring(0, urlHeader.IndexOf("?")); + } + + return urlHeader; + } + else + { + return "/"; + } + } + } + public string Method => _method; + public string Host => GetHeaderValue("X-Forwarded-Host"); + public int Port => _headers.ContainsKey("X-Forwarded-Port") ? Int32.Parse(GetHeaderValue("X-Forwarded-Port")) : 0; + public string UserAgent => GetHeaderValue("User-Agent"); +} diff --git a/dotnet8/Fission.DotNet.Common/FissionMqContext.cs b/dotnet8/Fission.DotNet.Common/FissionMqContext.cs new file mode 100644 index 00000000..8ce818a6 --- /dev/null +++ b/dotnet8/Fission.DotNet.Common/FissionMqContext.cs @@ -0,0 +1,15 @@ +using System; + +namespace Fission.DotNet.Common; + +public class FissionMqContext : FissionContext +{ + public FissionMqContext(Stream body, Dictionary arguments, Dictionary headers, Dictionary parameters) : base(body, arguments, headers, parameters) + { + + } + + public string Topic => GetHeaderValue("Topic"); + public string ErrorTopic => GetHeaderValue("Errortopic"); + public string ResponseTopic => GetHeaderValue("Resptopic"); +} diff --git a/dotnet8/Fission.DotNet.Common/ICorsPolicy.cs b/dotnet8/Fission.DotNet.Common/ICorsPolicy.cs new file mode 100644 index 00000000..7f10a326 --- /dev/null +++ b/dotnet8/Fission.DotNet.Common/ICorsPolicy.cs @@ -0,0 +1,16 @@ +using System; + +namespace Fission.DotNet.Common +{ + public interface ICorsPolicy + { + void AllowAnyOrigin(); + void AllowAnyHeader(); + void AllowAnyMethod(); + void AllowCredentials(); + + void WithOrigin(string[] origin); + void WithHeader(string[] header); + void WithMethod(string[] method); + } +} diff --git a/dotnet8/Fission.DotNet.Common/ILogger.cs b/dotnet8/Fission.DotNet.Common/ILogger.cs new file mode 100644 index 00000000..0b58d4f8 --- /dev/null +++ b/dotnet8/Fission.DotNet.Common/ILogger.cs @@ -0,0 +1,14 @@ +using System; + +namespace Fission.DotNet.Common +{ + public interface ILogger + { + void LogInformation(string message); + void LogDebug(string message); + void LogWarning(string message); + void LogError(string message); + void LogCritical(string message); + void LogError(string message, Exception exception); + } +} \ No newline at end of file diff --git a/dotnet8/Fission.DotNet.Common/README.md b/dotnet8/Fission.DotNet.Common/README.md new file mode 100644 index 00000000..973384fd --- /dev/null +++ b/dotnet8/Fission.DotNet.Common/README.md @@ -0,0 +1,46 @@ +# Fission.DotNet.Common + +This project is a common library for the .NET environment of [Fission](https://fission.io/), a serverless plugin for Kubernetes. The library provides common functionalities and utilities to facilitate the development of serverless functions with Fission on .NET. + +## Main Features + +- **Common Utilities**: Provides a set of common utilities to simplify the development of serverless functions. +- **.NET 8 Compatibility**: Designed to work with **.NET 8 on Linux**, offering an updated and improved experience. +- **Multi-Assembly Management**: Supports projects composed of multiple linked assemblies, simplifying the deployment and integration process. + +## Inspiration + +This project is inspired by the official Fission environment for .NET Core 2.0 but focuses on improvements and updates requested by the community, allowing developers to work with newer versions of the .NET framework and complex projects that include multiple assemblies. + +## Usage + +1. **Add the library to your project**: + Add the NuGet package `Fission.DotNet.Common` to your .NET project. + + ```bash + dotnet add package Fission.DotNet.Common + +2. **Create the project**: +- Create a **class library project** in .NET. +- Add the NuGet package `Fission.DotNet.Common` to your project. +- Create a class with the following function: + + ```csharp + using Fission.DotNet.Common; + + public class MyFunction + { + public object Execute(FissionContext input) + { + return "Hello World"; + } + } + ``` +3. **Compression**: Compress the assemblies and related files into a ZIP file. + +4. **Deploy to Fission**: Use this library to deploy your project to Fission, leveraging the ability to manage multiple linked assemblies. After compressing your project into a ZIP file, you can create the function in Fission with the following command: + + ```bash + fission fn create --name --env dotnet8 --code --entrypoint :: + ``` + Replace `` with the name of your function and `` with the path to your ZIP file. \ No newline at end of file diff --git a/dotnet8/Fission.DotNet/Adapter/FissionLoggerAdapter.cs b/dotnet8/Fission.DotNet/Adapter/FissionLoggerAdapter.cs new file mode 100644 index 00000000..cca1072a --- /dev/null +++ b/dotnet8/Fission.DotNet/Adapter/FissionLoggerAdapter.cs @@ -0,0 +1,44 @@ +using System; +using Fission.DotNet.Common; + +namespace Fission.DotNet.Adapter; + +public class FissionLoggerAdapter : Common.ILogger +{ + private Microsoft.Extensions.Logging.ILogger _logger; + + public FissionLoggerAdapter(Microsoft.Extensions.Logging.ILogger logger) + { + this._logger = logger; + } + + public void LogCritical(string message) + { + _logger.LogCritical(message); + } + + public void LogDebug(string message) + { + _logger.LogDebug(message); + } + + public void LogError(string message, Exception exception) + { + _logger.LogError(exception, message); + } + + public void LogError(string message) + { + _logger.LogError(message); + } + + public void LogInformation(string message) + { + _logger.LogInformation(message); + } + + public void LogWarning(string message) + { + _logger.LogWarning(message); + } +} diff --git a/dotnet8/Fission.DotNet/Controllers/FunctionController.cs b/dotnet8/Fission.DotNet/Controllers/FunctionController.cs new file mode 100644 index 00000000..f282c6c4 --- /dev/null +++ b/dotnet8/Fission.DotNet/Controllers/FunctionController.cs @@ -0,0 +1,194 @@ +using System.Text; +using Fission.DotNet.Common; +using Microsoft.AspNetCore.Mvc; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.Loader; +using Fission.DotNet.Interfaces; +using System.Threading.Tasks.Dataflow; +using Fission.DotNet.Services; + +namespace Fission.DotNet.Controllers +{ + [ApiController] + [Route("/")] + public class FunctionController : Controller + { + private readonly ILogger _logger; + private readonly IFunctionService _functionService; + + public FunctionController(ILogger logger, IFunctionService functionService) + { + this._logger = logger; + this._functionService = functionService; + } + /// + /// Handle HTTP GET requests by forwarding to the Fission function. + /// + /// The function return value. + [HttpGet] + public async Task Get() => await this.Run(Request); + + /// + /// Handle HTTP POST requests by forwarding to the Fission function. + /// + /// The function return value. + [HttpPost] + public async Task Post() => await this.Run(Request); + + /// + /// Handle HTTP PUT requests by forwarding to the Fission function. + /// + /// The function return value. + [HttpPut] + public async Task Put() => await this.Run(Request); + + /// + /// Handle HTTP HEAD requests by forwarding to the Fission function. + /// + /// The function return value. + [HttpHead] + public async Task Head() => await this.Run(Request); + + /// + /// Handle HTTP OPTIONS requests by forwarding to the Fission function. + /// + /// The function return value. + [HttpOptions] + public async Task Options() => await this.Run(Request); + + /// + /// Handle HTTP DELETE requests by forwarding to the Fission function. + /// + /// The function return value. + [HttpDelete] + public async Task Delete() => await this.Run(Request); + + /// + /// Invokes the Fission function on behalf of the caller. + /// + /// + /// 200 OK with the Fission function return value; or 400 Bad Request with the exception message if an exception + /// occurred in the Fission function; or 500 Internal Server Error if the environment container has not yet been + /// specialized. + /// + private async Task Run(HttpRequest request) + { + _logger.LogInformation("FunctionController.Run"); + + _functionService.Load(); + try + { + if ((request.Method == "OPTIONS") && (request.Headers.ContainsKey("X-Forwarded-Proto"))) + { + var corsPolicy = _functionService.GetCorsPolicy(); + var headers = (corsPolicy as CorsPolicy).GetCorsHeaders(); + foreach (var header in headers) + { + Response.Headers.Add(header.Key, header.Value); + } + /*Response.Headers.Add("Access-Control-Allow-Origin", "*"); + Response.Headers.Add("Access-Control-Allow-Methods", "*"); + Response.Headers.Add("Access-Control-Allow-Headers", "*"); + Response.Headers.Add("Access-Control-Allow-Credentials", "true"); + Response.Headers.Add("Access-Control-Expose-Headers", "*");*/ + return Ok(); + } + + // Copy the body to a MemoryStream so it can be read again + request.EnableBuffering(); + using (var memoryStream = new MemoryStream()) + { + await request.Body.CopyToAsync(memoryStream); + memoryStream.Seek(0, SeekOrigin.Begin); + + request.Body = memoryStream; + + + Fission.DotNet.Common.FissionContext context = null; + try + { + var httpArgs = request.Query.ToDictionary(x => x.Key, x => (object)x.Value); + var headers = request.Headers.ToDictionary(x => x.Key, x => (string)x.Value); + var parameters = new Dictionary(); + + // Extract headers that start with "X-Fission-Params-" + foreach (var header in request.Headers) + { + if (header.Key.StartsWith("X-Fission-Params-")) + { + var key = header.Key.Substring("X-Fission-Params-".Length); + var value = header.Value.ToString(); + parameters[key] = value; + } + } + + if (request.Headers.ContainsKey("Topic")) + { + _logger.LogInformation("FunctionController.Run: FissionMqContext"); + context = new Fission.DotNet.Common.FissionMqContext(request.Body, httpArgs, headers, parameters); + } + else + { + if (request.Headers.ContainsKey("X-Forwarded-Proto")) + { + _logger.LogInformation("FunctionController.Run: FissionHttpContext"); + context = new Fission.DotNet.Common.FissionHttpContext(request.Body, request.Method, httpArgs, headers, parameters); + } + else + { + _logger.LogInformation("FunctionController.Run: FissionContext"); + context = new Fission.DotNet.Common.FissionContext(request.Body, httpArgs, headers, parameters); + } + } + + if (context is FissionHttpContext) + { + _logger.LogInformation($"Body stream: {context.Content.Position}-{context.Content.Length}"); + //var body = await (context as FissionHttpContext).ContentAsString(); + + _logger.LogInformation($"Request Method: {(context as FissionHttpContext).Method}"); + //_logger.LogInformation($"Request Body: {body}"); + _logger.LogInformation($"Request Arguments: {string.Join(", ", (context as FissionHttpContext).Arguments.Select(x => $"{x.Key}={x.Value}"))}"); + } + _logger.LogInformation($"Request Method: {request.Method}"); + _logger.LogInformation($"Request Headers: {string.Join(", ", request.Headers.Select(x => $"{x.Key}={x.Value}"))}"); + _logger.LogInformation($"Request Query: {string.Join(", ", request.Query.Select(x => $"{x.Key}={x.Value}"))}"); + + } + catch (Exception ex) + { + _logger.LogError(ex, "FunctionController.Run"); + return BadRequest(ex.Message); + } + + try + { + var result = await _functionService.Execute(context); + if (context is FissionHttpContext) + { + var corsPolicy = _functionService.GetCorsPolicy(); + var headers = (corsPolicy as CorsPolicy).GetRequestCorsHeaders(); + foreach (var header in headers) + { + Response.Headers.Add(header.Key, header.Value); + } + } + //Response.Headers.Add("Access-Control-Allow-Origin", "*"); + return Ok(result); + } + catch (Exception ex) + { + _logger.LogError(ex, "FunctionController.Run"); + return BadRequest(ex.Message); + } + } + } + finally + { + _functionService.Unload(); + } + } + } +} + diff --git a/dotnet8/Fission.DotNet/Controllers/SpecializeController.cs b/dotnet8/Fission.DotNet/Controllers/SpecializeController.cs new file mode 100644 index 00000000..967502c3 --- /dev/null +++ b/dotnet8/Fission.DotNet/Controllers/SpecializeController.cs @@ -0,0 +1,57 @@ +using System.Text.Json; +using Microsoft.AspNetCore.Mvc; +using Fission.DotNet.Interfaces; +using Fission.DotNet.Model; + +namespace Fission.DotNet +{ + public class SpecializeController : Controller + { + private readonly ILogger logger; + private readonly ISpecializeService specializeService; + + public SpecializeController(ILogger logger, ISpecializeService specializeService) + { + this.logger = logger; + this.specializeService = specializeService; + } + + // GET: SpecializeController + [HttpPost, Route("/specialize")] + public Task Specialize() + { + logger.LogInformation("Specialize called"); + + return Task.FromResult(Results.Ok()); + } + + + [HttpPost, Route("/v2/specialize")] + public async Task SpecializeV2() + { + logger.LogInformation("SpecializeV2 called"); + + try + { + + + using (var reader = new StreamReader(Request.Body)) + { + var body = await reader.ReadToEndAsync(); + logger.LogInformation($"Body: {body}"); + + var request = JsonSerializer.Deserialize(body); + + specializeService.Specialize(request); + + return Task.FromResult(Results.Ok()); + } + } + catch (Exception ex) + { + logger.LogError(ex, "Error when specializing"); + return Task.FromResult(Results.BadRequest(ex.Message)); + } + } + } +} diff --git a/dotnet8/Fission.DotNet/Fission.DotNet.csproj b/dotnet8/Fission.DotNet/Fission.DotNet.csproj new file mode 100644 index 00000000..8b901b5d --- /dev/null +++ b/dotnet8/Fission.DotNet/Fission.DotNet.csproj @@ -0,0 +1,18 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + diff --git a/dotnet8/Fission.DotNet/Interfaces/IFunctionService.cs b/dotnet8/Fission.DotNet/Interfaces/IFunctionService.cs new file mode 100644 index 00000000..381cc9ab --- /dev/null +++ b/dotnet8/Fission.DotNet/Interfaces/IFunctionService.cs @@ -0,0 +1,12 @@ +using System; +using Fission.DotNet.Common; + +namespace Fission.DotNet.Interfaces; + +public interface IFunctionService +{ + void Load(); + void Unload(); + Task Execute(FissionContext context); + ICorsPolicy GetCorsPolicy(); +} diff --git a/dotnet8/Fission.DotNet/Interfaces/IFunctionStoreService.cs b/dotnet8/Fission.DotNet/Interfaces/IFunctionStoreService.cs new file mode 100644 index 00000000..e1daad1e --- /dev/null +++ b/dotnet8/Fission.DotNet/Interfaces/IFunctionStoreService.cs @@ -0,0 +1,10 @@ +using System; +using Fission.DotNet.Model; + +namespace Fission.DotNet.Interfaces; + +public interface IFunctionStoreService +{ + void SetFunction(FunctionStore function); + FunctionStore GetFunction(); +} diff --git a/dotnet8/Fission.DotNet/Interfaces/ISpecializeService.cs b/dotnet8/Fission.DotNet/Interfaces/ISpecializeService.cs new file mode 100644 index 00000000..bf42f4d6 --- /dev/null +++ b/dotnet8/Fission.DotNet/Interfaces/ISpecializeService.cs @@ -0,0 +1,9 @@ +using System; +using Fission.DotNet.Model; + +namespace Fission.DotNet.Interfaces; + +public interface ISpecializeService +{ + void Specialize(FissionSpecializeRequest request); +} diff --git a/dotnet8/Fission.DotNet/Model/FissionSpecializeRequest.cs b/dotnet8/Fission.DotNet/Model/FissionSpecializeRequest.cs new file mode 100644 index 00000000..f545aa16 --- /dev/null +++ b/dotnet8/Fission.DotNet/Model/FissionSpecializeRequest.cs @@ -0,0 +1,10 @@ +using System; + +namespace Fission.DotNet.Model; + +public class FissionSpecializeRequest +{ + public string filepath { get; set; } + public string functionName { get; set; } + public string url { get; set; } +} diff --git a/dotnet8/Fission.DotNet/Model/FunctionStore.cs b/dotnet8/Fission.DotNet/Model/FunctionStore.cs new file mode 100644 index 00000000..2a3069a8 --- /dev/null +++ b/dotnet8/Fission.DotNet/Model/FunctionStore.cs @@ -0,0 +1,27 @@ +using System; + +namespace Fission.DotNet.Model; + +public class FunctionStore +{ + public FunctionStore(string function) + { + if (string.IsNullOrEmpty(function)) + { + throw new ArgumentException("Function string cannot be null or empty", nameof(function)); + } + + var parts = function.Split(':'); + if (parts.Length != 3) + { + throw new ArgumentException("Function string must contain exactly three parts separated by dots", nameof(function)); + } + + Assembly = $"{parts[0]}.dll"; + Namespace = parts[1]; + FunctionName = parts[2]; + } + public string Assembly { get; private set; } + public string Namespace { get; private set; } + public string FunctionName { get; private set; } +} diff --git a/dotnet8/Fission.DotNet/Program.cs b/dotnet8/Fission.DotNet/Program.cs new file mode 100644 index 00000000..0b8aba7d --- /dev/null +++ b/dotnet8/Fission.DotNet/Program.cs @@ -0,0 +1,38 @@ +using System.ComponentModel; +using System.Text.Json; +using Fission.DotNet; +using Fission.DotNet.Adapter; +using Fission.DotNet.Interfaces; +using Fission.DotNet.Services; + +var builder = WebApplication.CreateBuilder(args); + +builder.WebHost.UseUrls("http://*:8888"); + +// Add services to the container. +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); +builder.Services.AddControllers(); + +builder.Services.AddSingleton(); +builder.Services.AddTransient(); +builder.Services.AddSingleton(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseRouting(); // Add the routing middleware + +app.UseEndpoints(endpoints => +{ + endpoints.MapControllers(); +}); + +app.Run(); diff --git a/dotnet8/Fission.DotNet/Properties/launchSettings.json b/dotnet8/Fission.DotNet/Properties/launchSettings.json new file mode 100644 index 00000000..657c5fa9 --- /dev/null +++ b/dotnet8/Fission.DotNet/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:24776", + "sslPort": 44362 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5074", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7199;http://localhost:5074", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/dotnet8/Fission.DotNet/Services/CorsPolicy.cs b/dotnet8/Fission.DotNet/Services/CorsPolicy.cs new file mode 100644 index 00000000..320cdd54 --- /dev/null +++ b/dotnet8/Fission.DotNet/Services/CorsPolicy.cs @@ -0,0 +1,115 @@ +using System; +using System.Collections.Generic; +using Fission.DotNet.Common; + +namespace Fission.DotNet.Services; + +public class CorsPolicy : ICorsPolicy +{ + private HashSet _origins = new HashSet(); + private HashSet _headers = new HashSet(); + private HashSet _methods = new HashSet(); + private bool _allowAnyOrigin = false; + private bool _allowAnyHeader = false; + private bool _allowAnyMethod = false; + private bool _allowCredentials = false; + + public void AllowAnyOrigin() + { + _allowAnyOrigin = true; + } + + public void AllowAnyHeader() + { + _allowAnyHeader = true; + } + + public void AllowAnyMethod() + { + _allowAnyMethod = true; + } + + public void AllowCredentials() + { + _allowCredentials = true; + } + + public void WithOrigin(string[] origins) + { + foreach (var origin in origins) + { + _origins.Add(origin); + } + } + + public void WithHeader(string[] headers) + { + foreach (var header in headers) + { + _headers.Add(header); + } + } + + public void WithMethod(string[] methods) + { + foreach (var method in methods) + { + _methods.Add(method); + } + } + + public Dictionary GetCorsHeaders() + { + var headers = new Dictionary(); + + if (_allowAnyOrigin) + { + headers["Access-Control-Allow-Origin"] = "*"; + } + else if (_origins.Count > 0) + { + headers["Access-Control-Allow-Origin"] = string.Join(", ", _origins); + } + + if (_allowAnyHeader) + { + headers["Access-Control-Allow-Headers"] = "*"; + } + else if (_headers.Count > 0) + { + headers["Access-Control-Allow-Headers"] = string.Join(", ", _headers); + } + + if (_allowAnyMethod) + { + headers["Access-Control-Allow-Methods"] = "*"; + } + else if (_methods.Count > 0) + { + headers["Access-Control-Allow-Methods"] = string.Join(", ", _methods); + } + + if (_allowCredentials) + { + headers["Access-Control-Allow-Credentials"] = "true"; + } + + return headers; + } + + public IDictionary GetRequestCorsHeaders() + { + var headers = new Dictionary(); + + if (_allowAnyOrigin) + { + headers["Access-Control-Allow-Origin"] = "*"; + } + else if (_origins.Count > 0) + { + headers["Access-Control-Allow-Origin"] = string.Join(", ", _origins); + } + + return headers; + } +} \ No newline at end of file diff --git a/dotnet8/Fission.DotNet/Services/CustomAssemblyLoadContext.cs b/dotnet8/Fission.DotNet/Services/CustomAssemblyLoadContext.cs new file mode 100644 index 00000000..dc751b27 --- /dev/null +++ b/dotnet8/Fission.DotNet/Services/CustomAssemblyLoadContext.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.Loader; + +namespace Fission.DotNet.Services +{ + public class CustomAssemblyLoadContext : AssemblyLoadContext + { + private AssemblyDependencyResolver _resolver; + + public CustomAssemblyLoadContext(string pluginPath, bool isCollectible = false) + : base(pluginPath, isCollectible) + { + _resolver = new AssemblyDependencyResolver(pluginPath); + } + + protected override Assembly? Load(AssemblyName assemblyName) + { + var assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName); + if (assemblyPath != null) + { + return LoadFromAssemblyPath(assemblyPath); + } + return null; + } + + protected override IntPtr LoadUnmanagedDll(string unmanagedDllName) + { + var libraryPath = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName); + if (libraryPath != null) + { + return LoadUnmanagedDllFromPath(libraryPath); + } + return IntPtr.Zero; + } + } +} \ No newline at end of file diff --git a/dotnet8/Fission.DotNet/Services/FunctionService.cs b/dotnet8/Fission.DotNet/Services/FunctionService.cs new file mode 100644 index 00000000..e867e2cb --- /dev/null +++ b/dotnet8/Fission.DotNet/Services/FunctionService.cs @@ -0,0 +1,377 @@ +using System; +using Fission.DotNet.Common; +using Fission.DotNet.Interfaces; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.Loader; +using Fission.DotNet.Adapter; + +namespace Fission.DotNet.Services; + +public class FunctionService : IFunctionService +{ + private readonly IFunctionStoreService _functionStoreService; + private readonly ILogger _logger; + private CustomAssemblyLoadContext _alc; + private WeakReference _alcWeakRef; + private Assembly _assemblyFunction; + private Type _classFunctionNameType; + + public FunctionService(IFunctionStoreService functionStoreService, ILogger logger) + { + this._functionStoreService = functionStoreService; + this._logger = logger; + } + + public ICorsPolicy GetCorsPolicy() + { + _logger.LogInformation("GetCorsPolicy"); + var policy = new CorsPolicy(); + + if (_classFunctionNameType != null) + { + // Method to execute + var executeMethod = _classFunctionNameType.GetMethod("SetCorsPolicy", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); + + if (executeMethod != null) + { + executeMethod.Invoke(null, new object[] { policy }); + var headers = policy.GetCorsHeaders(); + _logger.LogInformation($"CorsPolicy: {string.Join(", ", headers)}"); + } + } + else + { + throw new Exception("Type not found."); + } + + return policy; + } + + public Task Execute(FissionContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + /*var function = this._functionStoreService.GetFunction(); + + if (function == null) + { + throw new Exception("Function not specialized."); + } + + return ExecuteAndUnload(function.Assembly, function.Namespace, function.FunctionName, context);*/ + return ExecuteMethod(context); + } + + /*[MethodImpl(MethodImplOptions.NoInlining)] + private async Task ExecuteAndUnload(string assemblyPath, string nameSpace, string classFunctionName, FissionContext context) + { + ExecuteMethod(context); + /*if (!System.IO.File.Exists($"/function/{assemblyPath}")) + { + throw new Exception($"File /function/{assemblyPath} not found."); + } + + // Delete the common library if it exists. This is to ensure that the latest version is always loaded. + if (File.Exists($"/function/Fission.DotNet.Common.dll")) + { + File.Delete($"/function/Fission.DotNet.Common.dll"); + } + + if (File.Exists($"/function/Microsoft.Extensions.DependencyInjection.Abstractions.dll")) + { + File.Delete($"/function/Microsoft.Extensions.DependencyInjection.Abstractions.dll"); + } + + WeakReference alcWeakRef = null; + + var alc = new CustomAssemblyLoadContext($"/function/{assemblyPath}", isCollectible: true); + try + { + var assemblyFunction = alc.LoadFromAssemblyPath($"/function/{assemblyPath}"); + + alcWeakRef = new WeakReference(alc, trackResurrection: true); + + // Ottieni tutte le classi nell'assembly + Type[] types = assemblyFunction.GetTypes(); + _logger.LogInformation("Elenco delle classi nell'assembly:"); + foreach (var type1 in types) + { + _logger.LogInformation(type1.FullName); + } + + _logger.LogInformation($"Class try found: {nameSpace}.{classFunctionName}"); + + var classFunctionNameType = assemblyFunction.GetType($"{nameSpace}.{classFunctionName}"); + + if (classFunctionNameType != null) + { + MethodInfo configureServicesMethod = classFunctionNameType.GetMethod("ConfigureServices", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); + ServiceCollection serviceCollection = null; + ServiceProvider serviceProvider = null; + if (configureServicesMethod != null) + { + serviceCollection = new ServiceCollection(); + configureServicesMethod.Invoke(null, new object[] { serviceCollection }); + serviceCollection.AddTransient(classFunctionNameType); + serviceProvider = serviceCollection.BuildServiceProvider(); + } + + // Method to execute + var executeMethod = classFunctionNameType.GetMethod("Execute"); + + if (executeMethod != null) + { + // Create an instance of the object + object classInstance = null; + + if (serviceProvider != null) + { + classInstance = serviceProvider.GetService(classFunctionNameType); + } + else + { + classInstance = Activator.CreateInstance(classFunctionNameType); + } + + if (classInstance == null) + { + throw new Exception("Instance not created."); + } + + var executeMethodParameters = new object[] { context }; + + _logger.LogDebug($"Executing {classFunctionNameType.FullName}.{executeMethod.Name}"); + + var parameters = executeMethod.GetParameters(); + if (parameters.Length > 1) + { + _logger.LogDebug($"Method {executeMethod.Name} has more than one parameter."); + if (parameters[1].ParameterType == typeof(Common.ILogger)) + { + executeMethodParameters = new object[] { context, new FissionLoggerAdapter(_logger) }; + } + } + + _logger.LogDebug($"Method {executeMethod.Name} has {parameters.Length} parameters."); + + // Execute the method + var result = executeMethod.Invoke(classInstance, executeMethodParameters); + + if (result is Task task) + { + _logger.LogInformation("Task found."); + await task.ConfigureAwait(false); + var taskType = task.GetType(); + if (taskType.IsGenericType) + { + return taskType.GetProperty("Result").GetValue(task); + } + return null; + } + else + { + _logger.LogInformation("Task not found."); + } + + return result; + } + else + { + throw new Exception("Method not found."); + } + } + else + { + throw new Exception("Type not found."); + } + } + finally + { + alc.Unload(); + + for (int i = 0; alcWeakRef.IsAlive && (i < 10); i++) + { + GC.Collect(); + GC.WaitForPendingFinalizers(); + } + }* + }*/ + + public void Load() + { + var function = this._functionStoreService.GetFunction(); + + if (function == null) + { + throw new Exception("Function not specialized."); + } + + LoadAssembly(function.Assembly, function.Namespace, function.FunctionName); + } + + public void Unload() + { + _alc.Unload(); + + for (int i = 0; _alcWeakRef.IsAlive && (i < 10); i++) + { + GC.Collect(); + GC.WaitForPendingFinalizers(); + } + } + + + + private void LoadAssembly(string assemblyPath, string nameSpace, string classFunctionName) + { + if (!System.IO.File.Exists($"/function/{assemblyPath}")) + { + throw new Exception($"File /function/{assemblyPath} not found."); + } + + // Delete the common library if it exists. This is to ensure that the latest version is always loaded. + if (File.Exists($"/function/Fission.DotNet.Common.dll")) + { + File.Delete($"/function/Fission.DotNet.Common.dll"); + } + + if (File.Exists($"/function/Microsoft.Extensions.DependencyInjection.Abstractions.dll")) + { + File.Delete($"/function/Microsoft.Extensions.DependencyInjection.Abstractions.dll"); + } + + _alc = new CustomAssemblyLoadContext($"/function/{assemblyPath}", isCollectible: true); + _assemblyFunction = _alc.LoadFromAssemblyPath($"/function/{assemblyPath}"); + _alcWeakRef = new WeakReference(_alc, trackResurrection: true); + + _classFunctionNameType = _assemblyFunction.GetType($"{nameSpace}.{classFunctionName}"); + + if (_classFunctionNameType == null) + { + throw new Exception("Type not found."); + } + } + + + [MethodImpl(MethodImplOptions.NoInlining)] + private async Task ExecuteMethod(FissionContext context) + { + if (_classFunctionNameType != null) + { + MethodInfo configureServicesMethod = _classFunctionNameType.GetMethod("ConfigureServices", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); + ServiceProvider serviceProvider = null; + if (configureServicesMethod != null) + { + var serviceCollection = new ServiceCollection(); + configureServicesMethod.Invoke(null, new object[] { serviceCollection }); + serviceCollection.AddTransient(_classFunctionNameType); + serviceProvider = serviceCollection.BuildServiceProvider(); + } + + // Method to execute + var executeMethod = _classFunctionNameType.GetMethod("Execute"); + + if (executeMethod != null) + { + // Create an instance of the object + object classInstance = null; + + if (serviceProvider != null) + { + classInstance = serviceProvider.GetService(_classFunctionNameType); + } + else + { + classInstance = Activator.CreateInstance(_classFunctionNameType); + } + + if (classInstance == null) + { + throw new Exception("Instance not created."); + } + + var executeMethodParameters = new object[] { context }; + + _logger.LogDebug($"Executing {_classFunctionNameType.FullName}.{executeMethod.Name}"); + + var parameters = executeMethod.GetParameters(); + if (parameters.Length > 1) + { + _logger.LogDebug($"Method {executeMethod.Name} has more than one parameter."); + if (parameters[1].ParameterType == typeof(Common.ILogger)) + { + executeMethodParameters = new object[] { context, new FissionLoggerAdapter(_logger) }; + } + } + + _logger.LogDebug($"Method {executeMethod.Name} has {parameters.Length} parameters."); + + // Execute the method + var result = executeMethod.Invoke(classInstance, executeMethodParameters); + + if (result is Task task) + { + _logger.LogInformation("Task found."); + await task.ConfigureAwait(false); + var taskType = task.GetType(); + if (taskType.IsGenericType) + { + return taskType.GetProperty("Result").GetValue(task); + } + return null; + } + else + { + _logger.LogInformation("Task not found."); + } + + return result; + } + else + { + throw new Exception("Method not found."); + } + } + else + { + throw new Exception("Type not found."); + } + + + /*MethodInfo method = _classFunctionNameType.GetMethod(methodName); + + if (method == null) + { + throw new Exception("Method not found."); + } + + object classInstance = Activator.CreateInstance(_classFunctionNameType); + var executeMethodParameters = new object[] { context }; + + var parameters = method.GetParameters(); + if (parameters.Length > 1 && parameters[1].ParameterType == typeof(Common.ILogger)) + { + executeMethodParameters = new object[] { context, new FissionLoggerAdapter(_logger) }; + } + + var result = method.Invoke(classInstance, executeMethodParameters); + + if (result is Task task) + { + await task.ConfigureAwait(false); + var taskType = task.GetType(); + if (taskType.IsGenericType) + { + return taskType.GetProperty("Result").GetValue(task); + } + return null; + } + + return result;*/ + } +} + diff --git a/dotnet8/Fission.DotNet/Services/FunctionStoreService.cs b/dotnet8/Fission.DotNet/Services/FunctionStoreService.cs new file mode 100644 index 00000000..cd00e202 --- /dev/null +++ b/dotnet8/Fission.DotNet/Services/FunctionStoreService.cs @@ -0,0 +1,19 @@ +using System; +using Fission.DotNet.Interfaces; +using Fission.DotNet.Model; + +namespace Fission.DotNet.Services; + +public class FunctionStoreService : IFunctionStoreService +{ + private FunctionStore _function; + public FunctionStore GetFunction() + { + return _function; + } + + public void SetFunction(FunctionStore function) + { + _function = function; + } +} diff --git a/dotnet8/Fission.DotNet/Services/SpecializeService.cs b/dotnet8/Fission.DotNet/Services/SpecializeService.cs new file mode 100644 index 00000000..5c7fae4d --- /dev/null +++ b/dotnet8/Fission.DotNet/Services/SpecializeService.cs @@ -0,0 +1,73 @@ +using System; +using System.IO.Compression; +using Fission.DotNet.Interfaces; +using Fission.DotNet.Model; + +namespace Fission.DotNet.Services; + +public class SpecializeService : ISpecializeService +{ + private readonly ILogger _logger; + private readonly IFunctionStoreService _functionStoreService; + + public SpecializeService(ILogger logger, IFunctionStoreService functionStoreService) + { + this._logger = logger; + this._functionStoreService = functionStoreService; + } + + public void Specialize(FissionSpecializeRequest request) + { + if (request == null) + { + throw new NullReferenceException("Request is null"); + } + + if (System.IO.File.Exists("/userfunc/deployarchive.tmp")) + { + _logger.LogInformation("Deploy archive exists"); + UnzipDeployArchive("/userfunc/deployarchive.tmp", "/function"); + } + else + { + if (System.IO.File.Exists("/userfunc/deployarchive")) + { + _logger.LogInformation("Deploy archive exists"); + UnzipDeployArchive("/userfunc/deployarchive", "/function"); + } + else + { + _logger.LogInformation("Deploy archive does not exist"); + throw new Exception("Deploy archive does not exist"); + } + } + + if (string.IsNullOrEmpty(request.functionName)) + { + throw new ArgumentNullException("Function name is required"); + } + + var functionStore = new FunctionStore(request.functionName); + _functionStoreService.SetFunction(functionStore); + } + + private void UnzipDeployArchive(string zipFilePath, string extractPath) + { + if (System.IO.File.Exists(zipFilePath)) + { + // Create the /function folder if it does not exist + if (!Directory.Exists(extractPath)) + { + Directory.CreateDirectory(extractPath); + } + + // Unzip the ZIP file into the /function folder + ZipFile.ExtractToDirectory(zipFilePath, extractPath); + _logger.LogInformation("Deploy archive unzipped to {ExtractPath}", extractPath); + } + else + { + _logger.LogWarning("Deploy archive does not exist at {ZipFilePath}", zipFilePath); + } + } +} diff --git a/dotnet8/Fission.DotNet/appsettings.Development.json b/dotnet8/Fission.DotNet/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/dotnet8/Fission.DotNet/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/dotnet8/Fission.DotNet/appsettings.json b/dotnet8/Fission.DotNet/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/dotnet8/Fission.DotNet/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/dotnet8/FissionDotnet.sln b/dotnet8/FissionDotnet.sln new file mode 100644 index 00000000..d1a12a46 --- /dev/null +++ b/dotnet8/FissionDotnet.sln @@ -0,0 +1,37 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Fission.DotNet", "Fission.DotNet\Fission.DotNet.csproj", "{9E7DDE43-2E95-4635-8374-D6EC29ACDFD7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestExternalLibrary", "TestExternalLibrary\TestExternalLibrary.csproj", "{97FF278E-E51C-454C-9CB1-F8639A92A0F4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Fission.DotNet.Common", "Fission.DotNet.Common\Fission.DotNet.Common.csproj", "{A2D84129-1FCF-4578-BFBF-CE6807A97960}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {9E7DDE43-2E95-4635-8374-D6EC29ACDFD7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9E7DDE43-2E95-4635-8374-D6EC29ACDFD7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9E7DDE43-2E95-4635-8374-D6EC29ACDFD7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9E7DDE43-2E95-4635-8374-D6EC29ACDFD7}.Release|Any CPU.Build.0 = Release|Any CPU + {97FF278E-E51C-454C-9CB1-F8639A92A0F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {97FF278E-E51C-454C-9CB1-F8639A92A0F4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {97FF278E-E51C-454C-9CB1-F8639A92A0F4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {97FF278E-E51C-454C-9CB1-F8639A92A0F4}.Release|Any CPU.Build.0 = Release|Any CPU + {A2D84129-1FCF-4578-BFBF-CE6807A97960}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A2D84129-1FCF-4578-BFBF-CE6807A97960}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A2D84129-1FCF-4578-BFBF-CE6807A97960}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A2D84129-1FCF-4578-BFBF-CE6807A97960}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {28125F0B-59C9-4E51-977E-F54B7CCE3707} + EndGlobalSection +EndGlobal diff --git a/dotnet8/Makefile b/dotnet8/Makefile new file mode 100644 index 00000000..ec1cd281 --- /dev/null +++ b/dotnet8/Makefile @@ -0,0 +1,8 @@ +PLATFORMS ?= linux/amd64 + +-include ../rules.mk + +.PHONY: all +all: dotnet-env-img + +dotnet-env-img: Dockerfile \ No newline at end of file diff --git a/dotnet8/README.md b/dotnet8/README.md new file mode 100644 index 00000000..34c6f1e8 --- /dev/null +++ b/dotnet8/README.md @@ -0,0 +1,219 @@ +# Fission .NET Environment + +This project is an environment for [Fission](https://fission.io/), a serverless plugin for Kubernetes. It is based on Fission's official .NET environment but introduces support for more recent versions of the .NET framework, as the original version is still limited to .NET Core 2.0. This environment is specifically designed for **.NET 8 on Linux**. + +## Main Features + +- **Support for newer versions of .NET**: This environment is designed to work with **.NET 8 on Linux**, offering developers an updated and improved experience compared to the official environment. + +- **Multi-assembly project management**: One of the key features of this environment is the ability to handle and run projects composed of multiple linked assemblies. The assemblies can be compressed into a ZIP file, simplifying the process of distribution and integration. + +## Inspiration + +This project is inspired by Fission's official environment for .NET Core 2.0 but focuses on improvements and updates requested by the community, allowing developers to work with newer versions of the .NET framework and complex projects that include multiple assemblies. + +## Rebuilding and pushing the image + +To rebuild the image you will have to install Docker with version higher than 17.05+ +in order to support multi-stage builds feature. + +### Rebuild containers + +Move to the directory containing the source and start the container build process: + +``` +docker build -t USER/dotnet-env . +``` + +After the build finishes push the new image to a Docker registry using the +standard procedure. + +## Usage + +1. **Add the environment to Fission**: + To add this .NET 8 environment to Fission, use the following command: + + ```bash + fission env create --name dotnet8 --image + ``` + +2. **Project creation**: + - Create a **class library project** in .NET. + - Add the **Fission NuGet package** to your project. + - Create a class with the following function: + + ```csharp + public object Execute(FissionContext input) + { + return "Hello World"; + } + ``` + +3. **Pre-compilation**: + To avoid performance issues during the cold start of the function, it is necessary to compile the source code externally. The resulting assemblies will be used by Fission. + +4. **Compression**: Compress the assemblies and related files into a ZIP file. + +5. **Deploy to Fission**: Use this environment to deploy your project to Fission, leveraging the ability to handle multiple linked assemblies. After compressing your project into a ZIP file, you can create the function in Fission with the following command: + + ```bash + fission fn create --name --env dotnet8 --code --entrypoint :: + ``` + Replace `` with the name of your function, and `` with the path to your ZIP file. + +### HTTP trigger + +#### C# Example + +```csharp +public class MyFunction +{ + public object Execute(FissionContext context) + { + if (context is FissionHttpContext request) + { + return $"Hello from HTTP trigger! Method: {request.Method}, URL: {request.Url}"; + } + return "Invalid context"; + } +} +``` + +#### CORS + +To make external calls, you need to enable CORS (Cross-Origin Resource Sharing). This allows your function to handle requests from different origins. For each HTTP trigger, you will need to create an additional trigger of type OPTIONS. + +#### C# Example + +```csharp +public class MyFunction +{ + public object Execute(FissionContext context) + { + if (context is FissionHttpContext request) + { + return $"Hello from HTTP trigger! Method: {request.Method}, URL: {request.Url}"; + } + return "Invalid context"; + } + + public static void SetCorsPolicy(ICorsPolicy policy) + { + policy.AllowAnyHeader(); + policy.AllowAnyMethod(); + policy.AllowCredentials(); + policy.AllowAnyOrigin(); + } +} +``` + +### Cron trigger + +#### C# Example + +```csharp +public class MyFunction +{ + public object Execute(FissionContext context) + { + return "Hello from Cron trigger!"; + } +} +``` + +### Queue trigger + +#### C# Example + +```csharp +public class MyFunction +{ + public object Execute(FissionContext context) + { + if (context is FissionMqContext request) + { + return $"Hello from Queue trigger! Topic: {request.Topic}"; + } + return "Invalid context"; + } +} +``` + +### Async/Await + +You can use asynchronous methods in your function by utilizing the `async` and `await` keywords. This allows you to perform asynchronous operations, such as I/O-bound tasks, without blocking the main thread. + +#### C# Example + +```csharp +public class MyFunction +{ + public async Task ExecuteAsync(FissionContext context) + { + await Task.Delay(1000); // Simulate an asynchronous operation + return "Hello from async function!"; + } +} +``` + +### Logging + +You can integrate logging into your function by using the `ILogger` interface provided by the `Fission.DotNet.Common` namespace. This allows you to log information, warnings, errors, and other messages. Below is an example of how logging is implemented in a class. + +#### C# Example + +```csharp +using Fission.DotNet.Common; + +public class MyFunction +{ + public async Task Execute(FissionContext input, ILogger logger) + { + logger.LogInformation("Function execution started."); + // ...function logic... + logger.LogInformation("Function execution completed."); + return "Hello with logging!"; + } +} +``` + +### Service collection + +You can use service registration to manage dependencies and create the function class via Inversion of Control (IoC). This allows you to inject services into your function class, making it easier to manage dependencies and improve testability. + +#### C# Example + +```csharp +using Fission.DotNet.Common; +using Microsoft.Extensions.DependencyInjection; + +public class MyFunction +{ + private readonly IService service; + + public MyFunction(IService service) + { + this.service = service; + } + public async Task Execute(FissionContext input, ILogger logger) + { + return await service.Execute(input, logger); + } + + public static void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(); + } +} + +``` + +## Requirements + +- Kubernetes cluster +- Fission installed +- .NET SDK 8 for Linux + +## Related NuGet Package + +This project uses the **[Fission.DotNet.Common](https://www.nuget.org/packages/Fission.DotNet.Common/)** package. It provides essential functionality for interacting with the Fission environment in .NET, making it easier to work with serverless functions. diff --git a/dotnet8/envconfig.json b/dotnet8/envconfig.json new file mode 100644 index 00000000..7597cdaf --- /dev/null +++ b/dotnet8/envconfig.json @@ -0,0 +1,22 @@ +[ + { + "builder": "", + "examples": "https://github.com/fission/environments/tree/master/dotnet8/examples", + "icon": "./logo/logo_NETcore.svg", + "image": "dotnet8-env", + "keywords": [], + "kind": "environment", + "maintainers": [ + { + "link": "https://github.com/lcsoft77", + "name": "lcsoft77" + } + ], + "name": "Dotnet 8 Environment", + "readme": "https://github.com/fission/environments/tree/master/dotnet8", + "runtimeVersion": "8.0.0", + "shortDescription": "Fission Dotnet 8.0.0 runtime uses Kestrel to host the internal web server. The runtime use dll external compiled on zip file.", + "status": "Stable", + "version": "1.1.0" + } +] diff --git a/dotnet8/examples/AsyncFunctionExample/AsyncFunctionExample.csproj b/dotnet8/examples/AsyncFunctionExample/AsyncFunctionExample.csproj new file mode 100644 index 00000000..d1758302 --- /dev/null +++ b/dotnet8/examples/AsyncFunctionExample/AsyncFunctionExample.csproj @@ -0,0 +1,12 @@ + + + + Library + net8.0 + + + + + + + diff --git a/dotnet8/examples/AsyncFunctionExample/MyFunction.cs b/dotnet8/examples/AsyncFunctionExample/MyFunction.cs new file mode 100644 index 00000000..411ec335 --- /dev/null +++ b/dotnet8/examples/AsyncFunctionExample/MyFunction.cs @@ -0,0 +1,11 @@ +using Fission.DotNet.Common; +using System.Threading.Tasks; + +public class MyFunction +{ + public async Task ExecuteAsync(FissionContext context) + { + await Task.Delay(1000); // Simulate an asynchronous operation + return "Hello from async function!"; + } +} diff --git a/dotnet8/examples/CronTriggerExample/CronTriggerExample.csproj b/dotnet8/examples/CronTriggerExample/CronTriggerExample.csproj new file mode 100644 index 00000000..d1758302 --- /dev/null +++ b/dotnet8/examples/CronTriggerExample/CronTriggerExample.csproj @@ -0,0 +1,12 @@ + + + + Library + net8.0 + + + + + + + diff --git a/dotnet8/examples/CronTriggerExample/MyFunction.cs b/dotnet8/examples/CronTriggerExample/MyFunction.cs new file mode 100644 index 00000000..2c1f4e69 --- /dev/null +++ b/dotnet8/examples/CronTriggerExample/MyFunction.cs @@ -0,0 +1,9 @@ +using Fission.DotNet.Common; + +public class MyFunction +{ + public object Execute(FissionContext context) + { + return "Hello from Cron trigger!"; + } +} diff --git a/dotnet8/examples/HttpTriggerExample/HttpTriggerExample.csproj b/dotnet8/examples/HttpTriggerExample/HttpTriggerExample.csproj new file mode 100644 index 00000000..d1758302 --- /dev/null +++ b/dotnet8/examples/HttpTriggerExample/HttpTriggerExample.csproj @@ -0,0 +1,12 @@ + + + + Library + net8.0 + + + + + + + diff --git a/dotnet8/examples/HttpTriggerExample/MyFunction.cs b/dotnet8/examples/HttpTriggerExample/MyFunction.cs new file mode 100644 index 00000000..2d3fc244 --- /dev/null +++ b/dotnet8/examples/HttpTriggerExample/MyFunction.cs @@ -0,0 +1,13 @@ +using Fission.DotNet.Common; + +public class MyFunction +{ + public object Execute(FissionContext context) + { + if (context is FissionHttpContext request) + { + return $"Hello from HTTP trigger! Method: {request.Method}, URL: {request.Url}"; + } + return "Invalid context"; + } +} diff --git a/dotnet8/examples/LoggingExample/LoggingExample.csproj b/dotnet8/examples/LoggingExample/LoggingExample.csproj new file mode 100644 index 00000000..d1758302 --- /dev/null +++ b/dotnet8/examples/LoggingExample/LoggingExample.csproj @@ -0,0 +1,12 @@ + + + + Library + net8.0 + + + + + + + diff --git a/dotnet8/examples/LoggingExample/MyFunction.cs b/dotnet8/examples/LoggingExample/MyFunction.cs new file mode 100644 index 00000000..32b6609b --- /dev/null +++ b/dotnet8/examples/LoggingExample/MyFunction.cs @@ -0,0 +1,13 @@ +using Fission.DotNet.Common; +using System.Threading.Tasks; + +public class MyFunction +{ + public async Task Execute(FissionContext input, ILogger logger) + { + logger.LogInformation("Function execution started."); + // ...function logic... + logger.LogInformation("Function execution completed."); + return "Hello with logging!"; + } +} diff --git a/dotnet8/examples/QueueTriggerExample/MyFunction.cs b/dotnet8/examples/QueueTriggerExample/MyFunction.cs new file mode 100644 index 00000000..fc079a74 --- /dev/null +++ b/dotnet8/examples/QueueTriggerExample/MyFunction.cs @@ -0,0 +1,13 @@ +using Fission.DotNet.Common; + +public class MyFunction +{ + public object Execute(FissionContext context) + { + if (context is FissionMqContext request) + { + return $"Hello from Queue trigger! Topic: {request.Topic}"; + } + return "Invalid context"; + } +} diff --git a/dotnet8/examples/QueueTriggerExample/QueueTriggerExample.csproj b/dotnet8/examples/QueueTriggerExample/QueueTriggerExample.csproj new file mode 100644 index 00000000..d1758302 --- /dev/null +++ b/dotnet8/examples/QueueTriggerExample/QueueTriggerExample.csproj @@ -0,0 +1,12 @@ + + + + Library + net8.0 + + + + + + +