integration.scripting.roslyn - grecosoft/NetFusion GitHub Wiki

Roslyn - Overview

The NetFusion.Roslyn assembly provides an implementation for executing scripts at runtime against a domain entity. The domain entity can be an instance of a static class or an entity implementing the IAttributedEntity interface. If the domain-entity supports the IAttributedEntity interface, newly calculated properties can be dynamically added at runtime.

nuget NetFusion.Roslyn
register services.CompositeAppBuilder().AddRoslyn().Compose();
types IEntityScriptingService, IEntityScriptMetaRepository, IAttributedEntity

Script Evaluation

Service components can apply a script against a domain-entity by using the IEntityScriptingService interface implementation.


IMAGE The NetFusion.Roslyn plug-in provides an implementation of this interface by delegating to Roslyn. The implementation was designed to allow simple runtime evaluations of both static and dynamic properties.


The NetFusion.Base assembly also defines a repository interface that can be implemented to read and save scripts. The code examples will use an an in-memory repository implementation.

# The following assumes Demo is your current working directory.
dotnet add ./src/Components/Demo.Infra/Demo.Infra.csproj package NetFusion.Roslyn

After the above NuGet package is added, the NetFusion.Roslyn plugin is added to the composite application as follows:

public void ConfigureServices(IServiceCollection services)
{
    services.CompositeContainer(_configuration, new SerilogExtendedLogger())

        .AddRoslyn() // Add this line.

        .AddPlugin<InfraPlugin>()
        .AddPlugin<AppPlugin>()
        .AddPlugin<DomainPlugin>()
        .AddPlugin<WebApiPlugin>()
        .Compose();

    services.AddControllers();
}

This example will create a student domain entity for illustration. The class also supports the IAttributedEntity interface allowing a set of dynamic properties to be associated with the domain entity. Add the following class to the "Entities" directory of the Demo.Domain project:

touch ./src/Components/Demo.Domain/Entities/Student.cs
nano ./src/Components/Demo.Domain/Entities/Student.cs

Note: This class my already exist from a prior example.

using NetFusion.Base.Entity;
using System.Collections.Generic;

namespace Demo.Domain.Entities
{
    public class Student : IAttributedEntity
    {
        private readonly List<int> _scores = new List<int>();

        public string FirstName { get; }
        public string LastName { get; }
        public int[] Scores => _scores.ToArray();

        public bool Passing { get; set; }

        public Student(string firstName, string lastName, IEnumerable<int> scores)
        {
            _scores.AddRange(scores);

            Attributes = new EntityAttributes();
            FirstName = firstName;
            LastName = lastName;
        }

        public IEntityAttributes Attributes { get; }

        public IDictionary<string, object> AttributeValues
        {
            get => Attributes.GetValues();
            set => Attributes.SetValues(value);
        }

        public void AddScore(int score)
        {
            _scores.Add(score);
        }
    }
}

Implementing the IAttributedEntity interface is not a requirement for evaluating Roslyn expressions against the domain entity at runtime. If the IAttributedEntity interface is not implemented, new properties corresponding to an evaluated expression's result can't be added. However, properties defined on the class can be updated by expressions.

Next, an in memory repository responsible for returning a list of expressions for the above domain entity will be defined.

Within the Demo.Domain project, add the following class containing in-memory scripts to the "Scripts" directory:

mkdir ./src/Components/Demo.Domain/Scripts
nano ./src/Components/Demo.Domain/Scripts/StudentScripts
using Demo.Domain.Entities;
using System.Collections.Generic;
using NetFusion.Base.Scripting;

namespace Demo.Domain.Scripts
{
    public static class StudentScripts
    {
        public static EntityScript[] GetScripts(bool setIdentityValue = true)
        {
            var scoreScriptExpressions = new List<EntityExpression>
            {
                new EntityExpression("Entity.Scores.Min()", 0, "MinScore"),
                new EntityExpression("Entity.Scores.Max()", 1, "MaxScore"),
                new EntityExpression("Entity.Scores.Sum()/Entity.Scores.Count()", 2, "AverageScore"),
                new EntityExpression("_.MaxScore - _.MinScore", 3, "Difference"),
                new EntityExpression("Entity.Passing = _.AverageScore >= _.PassingScore", 4)
            };

            var displayScriptExpressions = new List<EntityExpression>
            {
                new EntityExpression("$\"{Entity.FirstName} - {Entity.LastName}\"", 0, "DisplayName")
            };

            var displayScript = new EntityScript(
                setIdentityValue ? "38F9560F-A8E4-4A64-81A6-77C66FA927C9" : null,
                "default",
                typeof(Student).AssemblyQualifiedName,
                displayScriptExpressions.AsReadOnly());

            displayScript.ImportedNamespaces.Add("System");

            var calcScript = new EntityScript(
                setIdentityValue ? "B83FD639-4AAC-4CBC-AA17-645DEAC4147B" : null,
                "scoreCalcs",
                typeof(Student).AssemblyQualifiedName,
                scoreScriptExpressions.AsReadOnly());

            calcScript.InitialAttributes["PassingScore"] = 70;

            calcScript.ImportedNamespaces.Add("System");
            calcScript.ImportedNamespaces.Add("System.Linq");

            return new[] { calcScript, displayScript };
        }
    }
}

Within the "Repositories" directory of the Demo.Infra project, add the following class.

nano ./src/Components/Demo.Infra/Repositories/EntityScriptMetaRepository.cs
using Demo.Domain.Scripts;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NetFusion.Base.Scripting;

namespace Demo.Infra.Repositories
{
    public class EntityScriptMetaRepository : IEntityScriptMetaRepository
    {
        public Task<IEnumerable<EntityScript>> ReadAllAsync()
        {
            EntityScript[] entityScripts = StudentScripts.GetScripts();
            return Task.FromResult(entityScripts.AsEnumerable());
        }

        public Task<string> SaveAsync(EntityScript script)
        {
            throw new NotImplementedException();
        }
    }
}

The above class implements a repository returning the list of in-memory scripts.


IMAGE If you have been following the tutorial examples, this repository will automatically be used since the RepositoryModule registers all repository classes by convention. Otherwise, create a NetFusion PluginModule (or use an existing) and register the above repository for the implementation of IEntityScriptMetaRepository.


Note the following in the above defined script:

  • Within an expression, dynamic attribute domain-entity values are referenced using the "_." syntax.
  • A static domain-entity property is referenced using the "Entity." syntax.
  • A dynamically calculated attribute can be referenced in subsequent expressions (based on Sequence value).
  • If the result of the expression is to update a static property on the domain entity, the AttributeName is not specified, and the expression contains the assignment: "Entity.Passing = _.AverageScore > 70"
  • For dynamic values that are used in the expressions but might not be specified, such as PassingScore above, a default value can be specified using the InitialAttributes property. If the dynamic property is not specified on the domain entity, then the InitialAttributes default value is used.

A script is applied to an entity by using the IEntityScriptingService service implementation. The default implementation completes the following:

  • When a script is first accessed, it is compiled and cached for future use.
  • Executes a script with a specified name against the entity. If a script name is not specified, the script named "default" is applied. If a script name is specified, it is executed after first running the "default" named script if present.

IMAGE A default script can contain common calculations used by multiple other scripts.


The following shows executing the above script specified in memory:

await this.EntityScriptingSrv.ExecuteAsync(student, "scoreCalcs");

Next, a controller will be created execute the script against the entity. Add the following model to the Demo.WebApi project:

nano ./src/Demo.WebApi/Models/StudentInfo.cs
namespace Demo.WebApi.Models
{
    public class StudentInfo
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public int[] Scores { get; set; }
        public int? PassingScore { get; set; }
    }
}

Add the following controller to the Demo.WebApi project to create an instance of the domain entity to which a script will be applied:

nano  ./src/Demo.WebApi/Controllers/ScriptController.cs
using System.Collections.Generic;
using System.Threading.Tasks;
using Demo.Domain.Entities;
using Demo.WebApi.Models;
using Microsoft.AspNetCore.Mvc;
using NetFusion.Base.Scripting;

namespace Demo.WebApi.Controllers
{
    [Route("api/[controller]")]
    public class ScriptController : Controller
    {
        private IEntityScriptingService Scripting { get; }

        public ScriptController(IEntityScriptingService scripting)
        {
            Scripting = scripting;
        }

        [HttpPost("apply/calcs")]
        public async Task<IDictionary<string, object>> ApplyCalcs([FromBody]StudentInfo model)
        {
            var student = new Student(
                model.FirstName,
                model.LastName,
                model.Scores);

            student.Attributes.Values.PassingScore = model.PassingScore;

            await Scripting.ExecuteAsync(student, "scoreCalcs");

            var resultModel = new Dictionary<string, object>(student.AttributeValues);
            resultModel["Passing"] = student.Passing;
            return resultModel;
        }
    }
}

Run the WebApi service project and summit a request that will apply the two defined scripts to the domain entity.

cd ./src/Domain.WebApi
dotnet run

The following shows the results of the execution:

IMAGE

As shown in the above port, the IEntityScriptingService is used to execute a script against a domain entity. This service exposes the following methods used to execute scripts:

public interface IEntityScriptingService
{
   void Load(IEnumerable<EntityScript> scripts);
   void CompileAllScripts();
   Task Execute(object entity, string scriptName = "default");
   Task<bool> SatifiesPredicate(object entity, ScriptPredicate predicate);
}

The last method above will execute a script resulting in the assignment of a dynamic predicate attribute (boolean property) used to determine if the domain entity meets the expression's criteria. In NetFusion.Messaging plug-in, the ApplyScriptPredicate attribute can be specified on a message handler to indicate a script executed to determine if the message should be handled:

[InProcessHandler, ApplyScriptPredicate("test-script", "IsImportant")]
public void OnEventPredicatePassed(MockEvalDomainEvent evt)
{
   // ...
}
⚠️ **GitHub.com Fallback** ⚠️