Validation and #Script together?

Is it possible to use Validation and #Script together? In particular I’m interested in being able to define validation rules at runtime, without needing recompile. So for example, for a particular form, the validation logic would call a service (database or web api) to grab a Sharp Script that would be used by the validation logic to then perform the validation as if the validation rules had been written in the traditional sense.

This would be REALLY awesome if we can get this to work. Is it possible? If so, a quick example would be greatly appreciated.

If you don’t want to recompile you’ll need to store the #Script outside of the code-base, e.g in a remote AppSettings configuration source otherwise you’ll need to fetch it yourself in the DataSource your App is using (e.g. a Validation Scripts table).

To execute a script you can either use your App’s SharpPagesFeature context gives your scripts access to the same functionality as your #Script Pages, otherwise you can execute them in an empty sandboxed context, e.g:

var context = HostContext.GetPlugin<SharpPagesFeature>();
var context = new ScriptContext().Init(); // empty sandboxed context

Then there is a matter for where to execute them, if you execute them in a Request Filter then you’ll be paying for an I/O request to fetch the script for each request, instead I’d add the #Script execution code in a base class OnBeforeExecute() method that is only inherited by Services which use dynamic validation that way the lookup only occurs for Services that have dynamic validation.

In this case I would execute the Script using a PageResult directly passing in the Request DTO into the PageResult.Model which will allow properties of the Request DTO to be accessed using just the property name Name as well as from the model.Name. Since your not interested in the output of the Service I’d just render it to a null stream as you’ll only be interested if Script throws an Exception:

public class DynamicValidationServices : Service
{
    public override void OnBeforeExecute(object requestDto)
    {
        try
        {
            // check if there's a script for this service
            var script = TryResolve<IAppSettings>().GetString(
                $"script.{requestDto.GetType().Name}.validation");
            if (script == null)
                return;
            
            var context = HostContext.GetPlugin<SharpPagesFeature>();            
            var pageResult = new PageResult(context.OneTimePage(script)) {
                Model = requestDto
            };
            pageResult.WriteToAsync(Stream.Null);
        }
        catch (ScriptException e)
        {
            throw e.InnerException ?? e;
        }
    }
}

Now you can write some #Script to validate your Request DTO whose properties you can access via Model.Prop or Prop, here’s a couple of examples:

```code
['Name','Age'] | to => requiredProps
#each requiredProps
    #if !model[it]
        it.throwArgumentNullException()
    /if
/each

(Age < 13) | ifThrowArgumentException("Must be 13 or over", "Age")
```

You can find different Exceptions you can throw from #Script Error Handling Page, you’ll likely also want your scripts to access some common validation functionality which you can make available in your scripts with:

var pageResult = new PageResult(context.OneTimePage(script)) {
    ScriptMethods = { new MyValidationScripts() },
    Model = requestDto
};

Note: I’ve rewritten #Script code block support in the latest v5.6.1 on MyGet where instead of it being implemented as a preprocessor it’s now parsed into a proper AST tree which allows faster/smarter execution of code statement blocks.

It uses the same syntax i.e. can execute #Script expressions without wrapping them in template expression syntax {{ ... }}, but is now smarter where it lets you execute “Verbatim” Blocks (e.g. raw/noop/csv/keyvalues) without needing to escape them and lets you execute template-only blocks (e.g. capture,capture,eval,html blocks) within code statements which previously wasn’t possible.

It also let you execute code blocks directly (i.e. without needing them wrap them in code doc comments) which you’ll most likely want to do when executing #Script and you don’t care about its rendered output, so you can just write:

['Name','Age'] | to => requiredProps
#each requiredProps
    #if !model[it]
        it.throwArgumentNullException()
    /if
/each

(Age < 13) | ifThrowArgumentException('Must be 13 or over', 'Age')

To execute code blocks you’ll need to use the new CodeSharpPage API, e.g:

public class DynamicValidationServices : Service
{
    public override void OnBeforeExecute(object requestDto)
    {
        try
        {
            // check if there's a script for this service
            var script = TryResolve<IAppSettings>().GetString(
                $"script.{requestDto.GetType().Name}.validation");
            if (script == null)
                return;
            
            var context = HostContext.GetPlugin<SharpPagesFeature>();            
            var pageResult = new PageResult(context.CodeSharpPage(script)) {
                Model = requestDto
            };
            pageResult.RenderToStream(Stream.Null);
        }
        catch (ScriptException e)
        {
            throw e.InnerException ?? e;
        }
    }
}

This is all sort of Greek to me but it sounds amazing! I’ll be giving it a try later today and let you know how it works. Thanks!

FYI context.CodeBlock() has been renamed to context.CodeSharpPage() for naming consistency in the latest v5.6.1.