SQL Server · Create
Turn ARX error codes into messages for the user
An "add account" screen that turns ARX errors into messages for the user, in four scenarios: constraints the database enforces, values the column can't hold, requests that don't fit the table, and the native details kept on a database error. Every error has a stable code (the same on every database), a category, and the entity, attribute or constraint involved. Messages name things, never values, so they are safe to log.
Set up for this page
The scenarios build on the sample application: SampleContext.cs (the dbContext every sample is given); SampleOutput.cs (Step and Show, which print); SampleContext.Application.cs (dbContext.Service, the application's ARX service); SampleCatalog.cs (the tables' ids, keys, lookups and display names).
Before scenario 1 the sample runs this, so the service and the rows the scenarios use are there:
var existing = new ArxEntity("account");
existing["name"] = "Litware Inc.";
existing["account_number"] = "ACC-20001";
await service.CreateAsync(existing);1 Constraints the database enforces
Constraints the database enforces: a unique number, a required name, a CHECK on the employee count.
Already in scope: service (ArxMsSqlDbService)
var numberTaken = new ArxEntity("account");
numberTaken["name"] = "Litware Europe";
numberTaken["account_number"] = "ACC-20001";
var noName = new ArxEntity("account");
noName["website"] = "https://no-name.example";
var negativeCount = new ArxEntity("account");
negativeCount["name"] = "Shrinking Co.";
negativeCount["employee_count"] = -5;
foreach (var attempt in new[] { numberTaken, noName, negativeCount })
{
await SaveAndExplainAsync(service, attempt);
}Defined once in this sample.
private static async Task SaveAndExplainAsync(ArxMsSqlDbService service, ArxEntity attempt)
{
Console.WriteLine($"save {attempt["name"] ?? "(no name)"}");
try
{
await service.CreateAsync(attempt);
}
catch (ArxException error)
{
// Handle by Code or Category, never by parsing the message.
var forTheUser = error.Code switch
{
ArxErrorCode.UniqueViolation => "That account number is already in use.",
ArxErrorCode.NotNullViolation => $"Please fill in {error.AttributeName}.",
ArxErrorCode.CheckViolation => "One of the values isn't allowed.",
ArxErrorCode.ValueTooLong => "One of the values is too long.",
ArxErrorCode.ValueOutOfRange => "One of the numbers is too large.",
ArxErrorCode.DataTypeMismatch or ArxErrorCode.InvalidValueFormat => "One of the values has the wrong format.",
_ when error.IsTransient => "The database is busy. Please try again.",
_ => "Something went wrong. The error was logged."
};
Console.WriteLine($" {error.Code} ({error.Category}), constraint {error.ConstraintName ?? "-"}, attribute {error.AttributeName ?? "-"}");
Console.WriteLine($" logged: {error.Message}");
Console.WriteLine($" user sees: {forTheUser}");
}
}Check it in the database
Run it in any SQL tool connected to the database, after the code has run.
SELECT name, account_number, employee_count
FROM account
WHERE account_number = 'ACC-20001' OR name IN ('Litware Europe', 'Shrinking Co.') OR website = 'https://no-name.example';Expect: 1 row, Litware Inc.: the three attempts were refused.
2 Values the column can't hold
Values the column can't hold: too long, out of range, the wrong type.
Already in scope: service (ArxMsSqlDbService)
var longWebsite = new ArxEntity("account");
longWebsite["name"] = "Long URL Ltd.";
longWebsite["website"] = "https://example.com/" + new string('a', 200);
var hugeCount = new ArxEntity("account");
hugeCount["name"] = "Huge Corp.";
hugeCount["employee_count"] = 3_000_000_000L;
var textForNumber = new ArxEntity("account");
textForNumber["name"] = "Typo Corp.";
textForNumber["employee_count"] = "forty";
foreach (var attempt in new[] { longWebsite, hugeCount, textForNumber })
{
await SaveAndExplainAsync(service, attempt);
}Defined once in this sample.
private static async Task SaveAndExplainAsync(ArxMsSqlDbService service, ArxEntity attempt)
{
Console.WriteLine($"save {attempt["name"] ?? "(no name)"}");
try
{
await service.CreateAsync(attempt);
}
catch (ArxException error)
{
// Handle by Code or Category, never by parsing the message.
var forTheUser = error.Code switch
{
ArxErrorCode.UniqueViolation => "That account number is already in use.",
ArxErrorCode.NotNullViolation => $"Please fill in {error.AttributeName}.",
ArxErrorCode.CheckViolation => "One of the values isn't allowed.",
ArxErrorCode.ValueTooLong => "One of the values is too long.",
ArxErrorCode.ValueOutOfRange => "One of the numbers is too large.",
ArxErrorCode.DataTypeMismatch or ArxErrorCode.InvalidValueFormat => "One of the values has the wrong format.",
_ when error.IsTransient => "The database is busy. Please try again.",
_ => "Something went wrong. The error was logged."
};
Console.WriteLine($" {error.Code} ({error.Category}), constraint {error.ConstraintName ?? "-"}, attribute {error.AttributeName ?? "-"}");
Console.WriteLine($" logged: {error.Message}");
Console.WriteLine($" user sees: {forTheUser}");
}
}Check it in the database
Run it in any SQL tool connected to the database, after the code has run.
SELECT count(*)
FROM account
WHERE name IN ('Long URL Ltd.', 'Huge Corp.', 'Typo Corp.');Expect: 0.
3 Requests that don't fit the table
Requests that don't fit the table: a column it doesn't have, a table name that isn't a plain identifier.
Already in scope: service (ArxMsSqlDbService)
var unknownColumn = new ArxEntity("account");
unknownColumn["name"] = "Extra Field Corp.";
unknownColumn["fax_number"] = "555-0100";
var badTableName = new ArxEntity("account; DROP TABLE account");
badTableName["name"] = "Bad Table Name Corp.";
foreach (var attempt in new[] { unknownColumn, badTableName })
{
await SaveAndExplainAsync(service, attempt);
}Defined once in this sample.
private static async Task SaveAndExplainAsync(ArxMsSqlDbService service, ArxEntity attempt)
{
Console.WriteLine($"save {attempt["name"] ?? "(no name)"}");
try
{
await service.CreateAsync(attempt);
}
catch (ArxException error)
{
// Handle by Code or Category, never by parsing the message.
var forTheUser = error.Code switch
{
ArxErrorCode.UniqueViolation => "That account number is already in use.",
ArxErrorCode.NotNullViolation => $"Please fill in {error.AttributeName}.",
ArxErrorCode.CheckViolation => "One of the values isn't allowed.",
ArxErrorCode.ValueTooLong => "One of the values is too long.",
ArxErrorCode.ValueOutOfRange => "One of the numbers is too large.",
ArxErrorCode.DataTypeMismatch or ArxErrorCode.InvalidValueFormat => "One of the values has the wrong format.",
_ when error.IsTransient => "The database is busy. Please try again.",
_ => "Something went wrong. The error was logged."
};
Console.WriteLine($" {error.Code} ({error.Category}), constraint {error.ConstraintName ?? "-"}, attribute {error.AttributeName ?? "-"}");
Console.WriteLine($" logged: {error.Message}");
Console.WriteLine($" user sees: {forTheUser}");
}
}Check it in the database
Run it in any SQL tool connected to the database, after the code has run.
SELECT count(*) AS stored, CASE WHEN OBJECT_ID('dbo.account') IS NOT NULL THEN 1 ELSE 0 END AS account_table_still_there
FROM account
WHERE name IN ('Extra Field Corp.', 'Bad Table Name Corp.');Expect: 0, 1: nothing stored, and the hostile table name changed nothing.
4 Native details on a database error
ArxValidationException is found before the database is called; ArxDatabaseException is reported by SQL Server, with the native details kept.
Already in scope: service (ArxMsSqlDbService)
var numberTaken = new ArxEntity("account");
numberTaken["name"] = "Litware Europe";
numberTaken["account_number"] = "ACC-20001";
try
{
await service.CreateAsync(numberTaken);
}
catch (ArxDatabaseException error)
{
Console.WriteLine($"NativeCode {error.NativeCode}, InnerException {error.InnerException?.GetType().Name}, IsTransient {error.IsTransient}");
}Check it in the database
Run it in any SQL tool connected to the database, after the code has run.
SELECT count(*)
FROM account
WHERE account_number = 'ACC-20001';Expect: 1: still only Litware Inc.
See also
Run the complete sample
The code above comes from one program, built in a fresh project and run against the database. To run it yourself, here is every file and step.
The program, its files and how to run it
Run it locally
- .NET SDK: .NET SDK 10.0.100 or later 10.0.x (target
net10.0). - Create the project:
Terminal dotnet new console -n ArxSample -f net10.0 cd ArxSample - Install the packages:
Terminal dotnet add package ARX.SDK.SQL --version 1.0.0-rc.1 - Database: a SQL Server 2022 (tested with 16.0.4225.2) database with the sample schema.
- Configuration: set
ARX_SAMPLES_SQLSERVERto its connection string. Nothing is hard-coded. - Source code: add the files of the sample application this sample uses (
SampleContext.cs,SampleOutput.cs,SampleContext.Application.cs,SampleCatalog.cs), then replaceProgram.csand add the other file below, with the file names shown. - Run it:
Terminal dotnet run - Expected output: below.
The files
Runs the sample. Generated for this page; it reads the connection string from the environment.
using ArxSamples.MSSQL;
using ArxSamples.MSSQL.Operations.Create.RealCases;
// The connection string comes from the environment, never from the code (see "Configure the connection").
var connectionString = Environment.GetEnvironmentVariable("ARX_SAMPLES_SQLSERVER")
?? throw new InvalidOperationException("Set the ARX_SAMPLES_SQLSERVER environment variable to your SQL Server connection string.");
await new RealCase5_HandleErrors().RunAsync(new SampleContext(connectionString));
The sample, as it is in the repository.
using ARX.SDK;
using ARX.SDK.Services;
using static ArxSamples.MSSQL.SampleOutput;
namespace ArxSamples.MSSQL.Operations.Create.RealCases;
/// <summary>
/// An "add account" screen that turns ARX errors into messages for the user, in four
/// scenarios: constraints the database enforces, values the column can't hold, requests that don't fit the table, and
/// the native details kept on a database error. Every error has a stable code (the same on every database), a
/// category, and the entity, attribute or constraint involved. Messages name things, never values, so they are safe to log.
/// </summary>
public sealed class RealCase5_HandleErrors : ISample
{
public string Name => "create-real-5-handle-errors";
public string Title => "Turn ARX error codes into messages for the user";
public async Task RunAsync(SampleContext dbContext)
{
// An account whose number is already taken.
var existing = new ArxEntity("account");
existing["name"] = "Litware Inc.";
existing["account_number"] = "ACC-20001";
await dbContext.Service.CreateAsync(existing);
await Scenario1_ConstraintsTheDatabaseEnforcesAsync(dbContext);
await Scenario2_ValuesTheColumnCantHoldAsync(dbContext);
await Scenario3_RequestsThatDontFitTheTableAsync(dbContext);
await Scenario4_NativeDetailsOnADatabaseErrorAsync(dbContext);
}
/// <summary>1. Constraints the database enforces: a unique number, a required name, a CHECK on the employee count.</summary>
private static async Task Scenario1_ConstraintsTheDatabaseEnforcesAsync(SampleContext dbContext)
{
Step("1. Constraints the database enforces");
var numberTaken = new ArxEntity("account");
numberTaken["name"] = "Litware Europe";
numberTaken["account_number"] = "ACC-20001";
var noName = new ArxEntity("account");
noName["website"] = "https://no-name.example";
var negativeCount = new ArxEntity("account");
negativeCount["name"] = "Shrinking Co.";
negativeCount["employee_count"] = -5;
foreach (var attempt in new[] { numberTaken, noName, negativeCount })
{
await SaveAndExplainAsync(dbContext, attempt);
}
/* ===========================================================
* [DIRECT QUERY CHECK]
* -----------------------------------------------------------
* SELECT name, account_number, employee_count
* FROM account
* WHERE account_number = 'ACC-20001' OR name IN ('Litware Europe', 'Shrinking Co.') OR website = 'https://no-name.example';
* -- Expect: 1 row, Litware Inc.: the three attempts were refused.
* ===========================================================
*/
}
/// <summary>2. Values the column can't hold: too long, out of range, the wrong type.</summary>
private static async Task Scenario2_ValuesTheColumnCantHoldAsync(SampleContext dbContext)
{
Step("2. Values the column can't hold");
var longWebsite = new ArxEntity("account");
longWebsite["name"] = "Long URL Ltd.";
longWebsite["website"] = "https://example.com/" + new string('a', 200);
var hugeCount = new ArxEntity("account");
hugeCount["name"] = "Huge Corp.";
hugeCount["employee_count"] = 3_000_000_000L;
var textForNumber = new ArxEntity("account");
textForNumber["name"] = "Typo Corp.";
textForNumber["employee_count"] = "forty";
foreach (var attempt in new[] { longWebsite, hugeCount, textForNumber })
{
await SaveAndExplainAsync(dbContext, attempt);
}
/* ===========================================================
* [DIRECT QUERY CHECK]
* -----------------------------------------------------------
* SELECT count(*)
* FROM account
* WHERE name IN ('Long URL Ltd.', 'Huge Corp.', 'Typo Corp.');
* -- Expect: 0.
* ===========================================================
*/
}
/// <summary>3. Requests that don't fit the table: a column it doesn't have, a table name that isn't a plain identifier.</summary>
private static async Task Scenario3_RequestsThatDontFitTheTableAsync(SampleContext dbContext)
{
Step("3. Requests that don't fit the table");
var unknownColumn = new ArxEntity("account");
unknownColumn["name"] = "Extra Field Corp.";
unknownColumn["fax_number"] = "555-0100";
var badTableName = new ArxEntity("account; DROP TABLE account");
badTableName["name"] = "Bad Table Name Corp.";
foreach (var attempt in new[] { unknownColumn, badTableName })
{
await SaveAndExplainAsync(dbContext, attempt);
}
/* ===========================================================
* [DIRECT QUERY CHECK]
* -----------------------------------------------------------
* SELECT count(*) AS stored, CASE WHEN OBJECT_ID('dbo.account') IS NOT NULL THEN 1 ELSE 0 END AS account_table_still_there
* FROM account
* WHERE name IN ('Extra Field Corp.', 'Bad Table Name Corp.');
* -- Expect: 0, 1: nothing stored, and the hostile table name changed nothing.
* ===========================================================
*/
}
/// <summary>
/// 4. ArxValidationException is found before the database is called; ArxDatabaseException is reported by SQL Server,
/// with the native details kept.
/// </summary>
private static async Task Scenario4_NativeDetailsOnADatabaseErrorAsync(SampleContext dbContext)
{
Step("4. Native details on a database error");
var numberTaken = new ArxEntity("account");
numberTaken["name"] = "Litware Europe";
numberTaken["account_number"] = "ACC-20001";
try
{
await dbContext.Service.CreateAsync(numberTaken);
}
catch (ArxDatabaseException error)
{
Show($"NativeCode {error.NativeCode}, InnerException {error.InnerException?.GetType().Name}, IsTransient {error.IsTransient}");
}
/* ===========================================================
* [DIRECT QUERY CHECK]
* -----------------------------------------------------------
* SELECT count(*)
* FROM account
* WHERE account_number = 'ACC-20001';
* -- Expect: 1: still only Litware Inc.
* ===========================================================
*/
}
/// <summary>The screen's save: on an ARX error, logs it and shows the user a message chosen by code, never by parsing the text.</summary>
private static async Task SaveAndExplainAsync(SampleContext dbContext, ArxEntity attempt)
{
Show($"save {attempt["name"] ?? "(no name)"}");
try
{
await dbContext.Service.CreateAsync(attempt);
}
catch (ArxException error)
{
// Handle by Code or Category, never by parsing the message.
var forTheUser = error.Code switch
{
ArxErrorCode.UniqueViolation => "That account number is already in use.",
ArxErrorCode.NotNullViolation => $"Please fill in {error.AttributeName}.",
ArxErrorCode.CheckViolation => "One of the values isn't allowed.",
ArxErrorCode.ValueTooLong => "One of the values is too long.",
ArxErrorCode.ValueOutOfRange => "One of the numbers is too large.",
ArxErrorCode.DataTypeMismatch or ArxErrorCode.InvalidValueFormat => "One of the values has the wrong format.",
_ when error.IsTransient => "The database is busy. Please try again.",
_ => "Something went wrong. The error was logged."
};
Show($" {error.Code} ({error.Category}), constraint {error.ConstraintName ?? "-"}, attribute {error.AttributeName ?? "-"}");
Show($" logged: {error.Message}");
Show($" user sees: {forTheUser}");
}
}
}
Expected output
What dotnet run printed when the sample was verified, on a freshly created sample schema. Ids differ when the tables already hold rows.
> 1. Constraints the database enforces
save Litware Europe
UniqueViolation (Constraint), constraint account_account_number_key, attribute -
logged: ARX3001 UniqueViolation: Create on 'account' violates unique constraint 'account_account_number_key'.
user sees: That account number is already in use.
save (no name)
NotNullViolation (Constraint), constraint -, attribute name
logged: ARX3003 NotNullViolation: Create on 'account' has no value for required attribute 'name'.
user sees: Please fill in name.
save Shrinking Co.
CheckViolation (Constraint), constraint account_employee_count_check, attribute employee_count
logged: ARX3004 CheckViolation: Create on 'account' violates check constraint 'account_employee_count_check'.
user sees: One of the values isn't allowed.
> 2. Values the column can't hold
save Long URL Ltd.
ValueTooLong (Data), constraint -, attribute website
logged: ARX4002 ValueTooLong: Create on 'account' has a value longer than column 'website' allows.
user sees: One of the values is too long.
save Huge Corp.
ValueOutOfRange (Data), constraint -, attribute -
logged: ARX4001 ValueOutOfRange: Create on 'account' has a value outside its column's range.
user sees: One of the numbers is too large.
save Typo Corp.
InvalidValueFormat (Data), constraint -, attribute -
logged: ARX4003 InvalidValueFormat: Create on 'account' has a value in a format its column can't accept.
user sees: One of the values has the wrong format.
> 3. Requests that don't fit the table
save Extra Field Corp.
UndefinedAttribute (Schema), constraint -, attribute fax_number
logged: ARX5002 UndefinedAttribute: Create on 'account' refers to a column that doesn't exist or can't be written: Invalid column name 'fax_number'. If it's a column the schema catalog declares (the id, a key, a lookup), check its name there: ValidateSchemaAsync() lists every declared column the table doesn't have.
user sees: Something went wrong. The error was logged.
save Bad Table Name Corp.
SuspectedInjection (Validation), constraint -, attribute -
logged: ARX1017 SuspectedInjection: Entity name 'account; DROP TABLE account' looks like an SQL injection attempt (contains ';'). ARX refused it before sending anything to the database. Names must come from your code or a fixed list, never from user input.
user sees: Something went wrong. The error was logged.
> 4. Native details on a database error
NativeCode 2601, InnerException SqlException, IsTransient False