Not rated yet

Run your own C# after a submission

MegaForm can compile and run C# you write yourself, on the server, immediately after a submission is saved. This page is the reference: when to reach for it, what the script is handed, and what the language and the runtime will and will not accept. The worked recipes live on their own pages and are linked at the end.

Important

If you arrived from an older version of this page describing ctx.Db or ctx.Http: those no

longer exist. A script now calls platform APIs directly — SqlConnection, HttpClient,

UserController — with an ordinary using.


1. Why, and when

Start by not writing a script. Most of what has to happen after a submission is already configuration, and configuration survives an export, needs nobody to enable scripting, and cannot stop compiling after an upgrade.

If the requirement isUse
every submission goes to one endpointWebhook / API service task
every submission is mirrored into one tableForm Settings → Database
email, approval, create user, add role, branch on a valueworkflow nodes

What no node expresses is a decision with shape. A node does one thing to every submission. These are the shapes that keep arriving, and none of them is a node with more checkboxes:

  • The write is more than one row, and the second depends on the first. An order header whose generated key the line items need. No node holds a value between two writes.
  • The destination depends on the answer. Enterprise enquiries to one CRM, everyone else to the internal queue, with a different payload shape for each.
  • The number has to be right, not close. Tax by country and product class, a rate rounded the way your finance team rounds it, a discount table that lives in another system.
  • The rule is a lookup, not a value on the form. Whether this class code is still open, whether this account is past due, whether this postcode is inside the service area.
  • Two systems have to agree. Write locally, call the remote API, and record what the remote one answered so a person can reconcile it later.

The honest test: if you can describe the requirement as "for every submission, do X", a node already does it, and the node is the better answer — one fewer thing to read at review time. If the requirement is "it depends", this is the tool.

The trade is real. A script is code, running in your website's process under your website's identity, with no sandbox. Safety and responsibility sets out what a host is agreeing to before switching it on.


2. A first script

You write the body. ctx is the submission; there is no boilerplate to type.

var name    = ctx.GetString("full_name");
var country = ctx.GetString("country", "GB");
var rate    = country == "DE" ? 0.19m : 0.20m;
var total   = ctx.GetDecimal("order_total", 0m) * (1m + rate);

ctx.Log($"country={country} rate={rate} total={total:0.00}");
ctx.SetVariable("totalIncVat", total);

if (total > 1000m)
{
    ctx.Response.SuccessMessage = "Thank you " + name + ". An account manager will call you today.";
}

That script reads two answers, computes a third, records it on the run, and changes what this one visitor sees on the thank-you screen. Everything past that — writing a row, calling an API, sending a message, creating an account — is ordinary C# with a using, and is covered by the recipe pages.

Where the editor is

Builder → Design → Form Settings → Server Script. Save the form once first: a script belongs to a form, so there has to be a form to attach it to.

[hinh: The Server Script panel in Form Settings]

The panel lists the current form's field keys above the editor, so you do not have to remember whether it was full_name or fullname. It also holds the enable switch for this form, what happens when a run fails, the run timeout, and Save script, Check syntax and Test run. Compilation happens at Save, so mistakes come back on your own line numbers while you are still looking at the editor rather than on a visitor's submission.

The script is not part of the form's schema and does not travel with it. Saving the form does not save the script, and saving the script does not save the form. They are separate because they carry different authority — see §6.


3. What ctx holds

This is the whole surface. Nothing else exists on it.

Reading the submission

MemberWhat it is
ctx.DataIDictionary<string, object> of the submitted values; keys are case-insensitive
ctx.Has(fieldKey)bool — whether the field was submitted at all
ctx.GetString(key, fallback = "")value as text
ctx.GetDecimal(key, fallback = 0m)parsed decimal
ctx.GetInt(key, fallback = 0)parsed integer
ctx.GetBool(key, fallback = false)parsed boolean
ctx.GetDate(key)DateTime?null when absent or unparseable

Has and a fallback answer different questions. GetDecimal("discount", 0m) returns 0 both for "the visitor typed 0" and for "the field was never on the form"; Has("discount") tells them apart.

Which submission, and whose

MemberWhat it is
ctx.FormId, ctx.SubmissionId, ctx.PortalId, ctx.FormTitlewhich form, which stored row
ctx.UserId, ctx.UserName, ctx.UserEmailthe signed-in submitter — 0 and empty strings for an anonymous submission
ctx.IpAddressthe caller's address
ctx.UtcNowthe run's timestamp

Public forms are the normal case, so treat UserId == 0 as expected input rather than as an error.

Recording what happened

MemberWhat it is
ctx.Log(string)one line in the run record
ctx.Logsthe lines logged so far
ctx.SetVariable(key, value)a value carried onto the run record
ctx.Variablesthe variables set so far
ctx.Fail(string)marks the run failed

Changing the submission, and the visitor's experience

MemberWhat it is
ctx.SetValue(key, value)queues a change to a stored value — refused after the commit, see §5
ctx.PendingChangeswhat SetValue has queued
ctx.Response.SuccessMessagereplaces the thank-you text for this submission
ctx.Response.RedirectUrlsends this visitor somewhere else
ctx.Response.CustomDataextra data returned with this submit response

ctx.Response affects this submission only. It is not a form setting and it does not persist.

Where in the pipeline you are

MemberWhat it is
ctx.Stagewhich stage is running
ctx.CanAbortwhether the current stage can still refuse the submission

4. What is supported

The shape your body is compiled into

The body you type is spliced into this method:

// SIGNATURE ONLY — this is generated around your body; you never type it.
async Task RunAsync(SubmissionScriptContext ctx, CancellationToken ct)

So await works directly, ct is in scope, and a bare return; is a valid way to stop early. You never write the signature.

Pass ct to the calls you await. Cancellation is cooperative — a token you do not pass is a timeout that cannot fire.

Language version: C# 7.3

The compiler is pinned to C# 7.3. Modern syntax you may be used to is a compile error at Save:

// FRAGMENT — none of these lines compile in a script.
using var cn = new SqlConnection(cs);   // no using declarations; use using (…) { }
List<string> names = new();             // no target-typed new
var text = """raw""";                   // no raw string literals

Interpolated strings, nameof, tuples, pattern matching as of 7.3, async/await, expression-bodied members and local functions are all available. Top-level statements are not a thing here — your body already is a method body.

using System.Globalization;

// Local functions are allowed, which is usually enough to keep a longer script readable.
decimal Net(decimal gross, decimal rate)
{
    return decimal.Round(gross / (1m + rate), 2);
}

var net = Net(ctx.GetDecimal("order_total", 0m), 0.20m);
ctx.Log("net=" + net.ToString("0.00", CultureInfo.InvariantCulture));

Namespaces already in scope

Do not repeat these — they are imported for you:

System
System.Collections.Generic
System.Linq
System.Text
System.Threading
System.Threading.Tasks
MegaForm.Core.Scripting

A script may begin with its own using directives. They are lifted above the generated wrapper, so they belong at the very top of the body, and a trailing comment after the semicolon is fine:

using System.Net.Http;              // the CRM call below
using Newtonsoft.Json;

var payload = JsonConvert.SerializeObject(new
{
    name  = ctx.GetString("full_name"),
    email = ctx.GetString("email")
});

using (var http = new HttpClient())
using (var body = new StringContent(payload, Encoding.UTF8, "application/json"))
{
    var reply = await http.PostAsync("https://crm.contoso.com/api/leads", body, ct);
    ctx.Log($"crm status={(int)reply.StatusCode}");
    if (!reply.IsSuccessStatusCode) ctx.Fail("CRM rejected the lead");
}

Which libraries you can name

The reference set is every assembly the site has loaded. Anything a DNN module can name, a script can name — including:

NamespaceFor
DotNetNuke.*users, roles, mail, config, the platform's own services
System.Data.SqlClientyour own tables, with parameters
System.Net.HttpREST, SOAP, anything over HTTP
Newtonsoft.Jsonserialising and parsing payloads

Two limits sit on top of that:

  • unsafe code is refused — pointers and stackalloc — in every configuration.
  • An optional strict mode exists and is off by default. Turned on, it restores an older namespace deny-list that refuses most of the table above. It is a process-wide startup flag, not a per-form setting. Safety and responsibility is where it is documented; do not assume it is on.

Size and time

LimitValue
Source length64 KB
Run timeout10 seconds by default, 60 seconds maximum

Warning

The timeout bounds what the visitor waits for, not what the script does. .NET has no safe way

to abort work already running, so on expiry the request stops waiting and the submission

completes, while the script keeps its thread until it finishes or the application recycles. Treat

the timeout as a seatbelt, not a brake: bound your own loops, and pass ct to every call you

await.


5. Stages, and what they stop you doing

Four stages exist in the engine. They differ in where they sit relative to the database commit, which decides everything else.

StageRunsCan refuse the submissionCan change stored valuesConfigurable today
PreValidatebefore validation finishesyesyesno
PreInsertinside the submit transactionyesyesno
PostCommitafter the row is committednonoyes
AsyncWorkerlater, off a queuenonono

Important

Only PostCommit can be configured. The script you write and approve is stored as the form's

after-submit hook, and the engine runs that hook as PostCommit. Nothing in the product writes a

script into the other three stages — no editor, no API, no import path.

Two consequences, stated plainly because they are the biggest constraint on this page:

  • A script cannot refuse a submission. Refusing needs PreInsert. ctx.Fail("…") at PostCommit records the run as failed; the row is already stored. To turn away unwelcome-but-valid input, use the anti-spam settings and workflow rules.
  • A script cannot rewrite a stored value. ctx.SetValue is refused after the commit rather than quietly ignored, on purpose: a script that believes it corrected a stored value and did not is a data bug that surfaces months later in a report.

What a PostCommit script can do is compute a value and send it onward — into your own table, to an API, into an email, onto the run record, into ctx.Response. It just does not go back into the submission.


6. The three gates

A script does not run until all three hold. There is no fallback path and no per-site override.

1 — a config file switch. In web.config appSettings:

<appSettings>
  <add key="MegaForm:AfterSubmitScriptEnabled" value="true" />
</appSettings>

Off on every install. Anything other than true means off. It is a file rather than a settings screen on purpose: turning this on means people may run code on this server, and the right bar for that is can edit files on this server — a smaller group than knows the superuser password. It also means a site restored from a backup and a fresh config file comes back with the feature off.

2 — a host account saves the script. Host / SuperUser only. Not a site Administrator, not someone with Edit permission on the module. "Edit module" is a content-editor permission several people usually hold, and an Administrator's reach stops at one site while a script runs in the process shared by the whole installation.

3 — an approval hash over the source. Saving stores a hash of the exact source the server accepted, with who approved it and when. Before every run the server re-hashes and compares. Mismatch, no run.

That third gate is what makes every other route into your forms inert. A form export, a template install, a gallery download, a restored backup, a hand-built save request — all of them can carry a script's source, and none of them can carry a valid approval, because an approval is written server-side at the instant a host pressed Save on that site. The ordinary form Save path goes further: it discards the caller's copy of this block and writes back the one the server already had. A content editor saving a form cannot introduce a script, alter an approved one, or switch one off.


7. What is recorded when it runs

Every run is written down, and so is every approval. Between them they answer the two questions that only have answers if something wrote them at the time: what did this script do on that submission, and who put this code on the server.

A run keeps the form, the submission, the stage, the hash of the source that ran, success or failure, the duration, the error if there was one, and everything the script logged. A skipped run is not a failed one — disabled, empty, not approved on this site, or a hash mismatch each record a reason rather than an error.

This is what one measured run looks like from the log side. On a DNN 10.3 site, an anonymous submission ran a PostCommit script that did four things in 927 ms: a parameterised INSERT through SqlConnection, an HttpClient POST to an external endpoint, a message handed to DNN's own sender, and an account created with a role granted.

country=DE rate=0.19 total=892.50
insert rowsAffected=1
crm status=200
mail handed to DNN's sender for jane.carter@example.com

Four ctx.Log lines, one per effect, is a good habit: a script that catches everything and logs nothing will fail for months without anyone noticing. The tables, their columns, and the host-only endpoint that reads them are documented in Safety and responsibility.


8. Where to go next

Writing your own C# after a submission is the branch overview. Each recipe below is one job, with the script and what the run record shows afterwards.

RecipeWhat it covers
Write to your own databaseSqlConnection, parameters, several tables in one run
CRM, ERP, REST and SOAPHttpClient, payload shape, what to do with the reply
Email and messagingDNN's own sender, and calling a provider's API
Create users and grant rolesUserController, RoleController, and what to check first
Safety and responsibilitythe gates in detail, strict mode, and reviewing a script before you approve it
Was this page helpful?

0 comments

Comments are reviewed before they appear.