name: ewl-server-side-console-app description: Create server-side console apps and call them from EWL web apps or other apps in the system. Use when creating a new console app, background worker, bulk operations worker, batch job, scheduled task, background job, background process, worker process, offline job, offline processor, or Exe project in an EWL system.
Overview
A server-side console app is a standalone console project that performs long-running or bulk operations outside the web request lifecycle. EWL calls these "server-side console projects." The Development Utility generates:
- A
Programclass withMainthat initializes EWL, deserializes arguments, and calls apartial void ewlMainmethod you implement. - A
ServerSideConsoleAppStaticsclass in the Library with a typedStart<Name>method for launching the process.
Configuring the project in Development.xml
Add a serverSideConsoleProjects entry in Library\Configuration\Development.xml:
<serverSideConsoleProjects>
<project>
<Name>Bulk Operations Worker</Name>
<NamespaceAndAssemblyName>MySystem.BulkOperationsWorker</NamespaceAndAssemblyName>
</project>
</serverSideConsoleProjects>
The Name is the folder name for the project inside the solution root. The
NamespaceAndAssemblyName is the root namespace and assembly name.
Creating the project folder and csproj
Create a folder matching the Name (e.g. Bulk Operations Worker\) at the
solution root. Add a .csproj file:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Library\Library.csproj" />
</ItemGroup>
</Project>
The Directory.Build.props file (generated by sync) sets
the AssemblyName, RootNamespace, RuntimeIdentifier (win-x64), and
other standard properties. You do not need to set those in the .csproj.
Generated code
After running sync, the DU generates
Generated Code\Main.g.cs in the project folder containing:
namespace MySystem.BulkOperationsWorker;
internal static partial class Program {
private static int Main( string[] args ) {
var dataAccessState = new Lazy<DataAccessState>( () => new DataAccessState() );
GlobalInitializationOps.InitStatics(
new GlobalInitializer(), "Bulk Operations Worker", false,
mainDataAccessStateGetter: () => dataAccessState.Value! );
try {
return GlobalInitializationOps.ExecuteAppWithStandardExceptionHandling( () => {
Console.SetIn( new StreamReader( Console.OpenStandardInput(), Console.InputEncoding, false, 4096 ) );
ewlMain( /* deserialized arguments */ );
} );
}
finally {
GlobalInitializationOps.CleanUpStatics();
}
}
static partial void ewlMain( IReadOnlyList<string> arguments );
}
This handles EWL initialization, argument deserialization (JSON via stdin when invoked through the generated starter), and cleanup.
Implementing the worker
Create a Program.cs (or any .cs file) in the project folder with your
partial class Program implementing ewlMain:
namespace MySystem.BulkOperationsWorker;
partial class Program {
static partial void ewlMain( IReadOnlyList<string> arguments ) {
var organizationId = int.Parse( arguments[ 0 ] );
var userId = int.Parse( arguments[ 1 ] );
var filePath = arguments[ 2 ];
DataAccessState.Current.PrimaryDatabaseConnection.ExecuteWithConnectionOpen(
() => DataAccessState.Current.PrimaryDatabaseConnection.ExecuteInTransaction( () => {
// Perform bulk operations here
// Parse files, import records, send emails, etc.
} ) );
}
}
The arguments list is strongly typed as IReadOnlyList<string>. Parse each
positional argument as needed. The full EWL data access layer is available,
including table retrievals, modifications, and email.
Generated starter method
The DU also generates a ServerSideConsoleAppStatics class in the Library's
generated code. For a project named "Bulk Operations Worker", it generates:
public static class ServerSideConsoleAppStatics {
public static void StartBulkOperationsWorker(
IEnumerable<string> arguments, string input,
string errorMessageIfAlreadyRunning = "" ) { ... }
}
This method:
- If
errorMessageIfAlreadyRunningis non-empty, checks whether the process is already running and throwsDataModificationExceptionif so. - If called within an EWL web request (
EwfRequest.Current is not null), defers execution viaAutomaticDatabaseConnectionManager.AddNonTransactionalModificationMethod. This ensures the process launches after the current database transaction commits successfully. - If called outside an EWL web request, runs the process immediately and
synchronously via
ProcessTools.RunProgram.
Calling from an EWL web app
Call the generated starter method from a page's modification method. The process will start after the current transaction commits:
// EwlPage
partial class EventFileImport {
protected override PageContent getContent() =>
FormState.ExecuteWithActions(
PostBack.CreateFull(
modificationMethod: () => {
// Save the uploaded file to disk
var filePath = EwlStatics.CombinePaths( folderPath, fileName );
File.WriteAllBytes( filePath, uploadedFile.Contents );
// Launch the worker
ServerSideConsoleAppStatics.StartBulkOperationsWorker(
organizationId.ToString()
.ToCollection()
.Append( SystemUser.Current.UserId.ToString() )
.Append( filePath ),
"",
errorMessageIfAlreadyRunning:
"Another operation is already in progress." );
AddStatusMessage( StatusMessageType.Info,
"Import started. You will receive an email shortly." );
} ),
() => new UiPageContent( contentFootActions: new ButtonSetup( "Import" ) )
.Add( fileUploadComponents ) );
}
Because AddNonTransactionalModificationMethod is used under the hood, the
worker only launches if the web request's transaction commits. If validation
fails or an exception is thrown, the worker is never started.
Calling from an ASP.NET MVC app
In a non-EWL web app, call ProcessTools.RunProgram directly. There is no
EwfRequest.Current, so the generated starter runs the program synchronously.
You can either call the generated starter or invoke the process yourself:
using EnterpriseWebLibrary.TewlContrib;
// Option 1: Use the generated starter (runs synchronously in non-EWL context)
ServerSideConsoleAppStatics.StartBulkOperationsWorker(
new[] { organizationId.ToString(), userId.ToString(), filePath },
"" );
// Option 2: Launch directly if you need fire-and-forget behavior
var programPath = EwlStatics.CombinePaths(
ConfigurationStatics.InstallationConfiguration.InstallationPath,
"Bulk Operations Worker",
ConfigurationStatics.ServerSideConsoleAppRelativeFolderPath,
"MySystem.BulkOperationsWorker" );
ProcessTools.RunProgram(
programPath,
"ewlUseJsonArguments",
JsonConvert.SerializeObject( arguments ) + Environment.NewLine + input,
false );
The ewlUseJsonArguments first argument tells the generated Main to
deserialize the argument list from JSON on stdin rather than using raw
command-line args.
Passing data to the worker
Arguments are passed as an IEnumerable<string> (serialized as JSON via
stdin). For simple values, use positional string arguments:
ServerSideConsoleAppStatics.StartBulkOperationsWorker(
new[] { organizationId.ToString(), userId.ToString(), filePath },
"" );
The second parameter (input) is additional stdin content appended after the
JSON arguments line. Use it for large data that does not fit in arguments.
Error handling and notifications
The worker runs as a separate process with its own EWL initialization. Use standard EWL patterns for error handling and email notifications:
static partial void ewlMain( IReadOnlyList<string> arguments ) {
DataAccessState.Current.PrimaryDatabaseConnection.ExecuteWithConnectionOpen(
() => DataAccessState.Current.PrimaryDatabaseConnection.ExecuteInTransaction( () => {
// Process data, collect errors
var errors = new List<DataValidationError>();
processRecords( errors );
// Send result email
var message = new EmailMessage();
message.ToAddresses.Add( new EmailAddress( user.EmailAddress, user.FriendlyName ) );
message.Subject = errors.Any() ? "Import failed" : "Import succeeded";
message.BodyHtml = buildResultHtml( errors );
GlobalStatics.SendEmail( message );
// Roll back transaction on failure
if( errors.Any() )
throw new DoNotCommitException();
}, createSavepointIfAlreadyInTransaction: true ) );
}
The DoNotCommitException pattern rolls back the transaction so no partial
data is committed, while still allowing the email to be sent before the
rollback (if using a non-transactional email method) or by sending the email
outside the transaction scope.
Run Sync
Run sync after adding or modifying the
serverSideConsoleProjects configuration in Development.xml. This
regenerates both the worker's Main.g.cs and the Library's
ServerSideConsoleAppStatics class. After running the DU for a new project,
add it to the solution with dotnet sln add.