MySQL · Create
Catch an error by its code
Catching errors, in three scenarios: a database error by its code, several database errors by their category, and a mistake ARX refuses before anything is sent to the database.
Set up for this page
Besides the sample itself, it uses SampleContext.cs (the dbContext every sample is given); SampleOutput.cs (Step and Show, which print), in the files below.
1 By its code
The usual way: the database refuses a record, and the ARX code says why. Here, a duplicate account number.
Already in scope: connectionString (string)
var service = Service(connectionString);
var original = new ArxEntity("account");
original["name"] = "Tailspin Toys";
original["account_number"] = "ACC-0005";
await service.CreateAsync(original);
// Account numbers are unique, so the database refuses this one.
var duplicate = new ArxEntity("account");
duplicate["name"] = "Tailspin Toys (copy)";
duplicate["account_number"] = "ACC-0005";
try
{
await service.CreateAsync(duplicate);
}
catch (ArxDatabaseException error) when (error.Code == ArxErrorCode.UniqueViolation)
{
Console.WriteLine($"not created: {error.Message}");
}Defined once in this sample.
private static ArxMySqlDbService Service(string connectionString) => new ArxMySqlDbService(connectionString, ArxSchemaCatalog.Create()
.Table("account", ArxIdColumn.Generated<long>("id"))
.Table("country", ArxIdColumn.Supplied<string>("code"))
.Build());Check it in the database
Run it in any SQL tool connected to the database, after the code has run.
SELECT name, account_number
FROM account
WHERE account_number = 'ACC-0005';Expect: 1 row, Tailspin Toys: the copy was refused.
2 By its category
The other way: catch by category when several codes need the same handling. Every constraint the database enforces (unique, not null, check, foreign key) is the Constraint category.
Already in scope: connectionString (string)
var service = Service(connectionString);
var numberTaken = new ArxEntity("account");
numberTaken["name"] = "Tailspin Toys (another copy)";
numberTaken["account_number"] = "ACC-0005";
var noName = new ArxEntity("account");
noName["account_number"] = "ACC-0013"; // name is required (NOT NULL)
foreach (var account in new[] { numberTaken, noName })
{
try
{
await service.CreateAsync(account);
}
catch (ArxDatabaseException error) when (error.Category == ArxErrorCategory.Constraint)
{
Console.WriteLine($"{error.Category}: {error.Code}");
}
}Defined once in this sample.
private static ArxMySqlDbService Service(string connectionString) => new ArxMySqlDbService(connectionString, ArxSchemaCatalog.Create()
.Table("account", ArxIdColumn.Generated<long>("id"))
.Table("country", ArxIdColumn.Supplied<string>("code"))
.Build());Check it in the database
Run it in any SQL tool connected to the database, after the code has run.
SELECT account_number, count(*)
FROM account
WHERE account_number IN ('ACC-0005', 'ACC-0013')
GROUP BY account_number;Expect: only ACC-0005, count 1: neither refused account was stored.
3 Before the database
A mistake in the request itself is refused by ARX before anything is sent: an ArxValidationException, not an ArxDatabaseException. Here, a country without its code, which the schema declares as supplied.
Already in scope: connectionString (string)
var service = Service(connectionString);
var noCode = new ArxEntity("country");
noCode["name"] = "Narnia";
try
{
await service.CreateAsync(noCode);
}
catch (ArxValidationException error) when (error.Code == ArxErrorCode.InvalidArgument)
{
Console.WriteLine($"{error.Code}: {error.Message}");
}Defined once in this sample.
private static ArxMySqlDbService Service(string connectionString) => new ArxMySqlDbService(connectionString, ArxSchemaCatalog.Create()
.Table("account", ArxIdColumn.Generated<long>("id"))
.Table("country", ArxIdColumn.Supplied<string>("code"))
.Build());Check it in the database
Run it in any SQL tool connected to the database, after the code has run.
SELECT count(*)
FROM country
WHERE name = 'Narnia';Expect: 0: refused before anything was sent.
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.MySQL --version 1.0.0-rc.1 - Database: a MySQL 8.4 (tested with 8.4.13) database with the sample schema.
- Configuration: set
ARX_SAMPLES_MYSQLto its connection string. Nothing is hard-coded. - Source code: replace
Program.csand add the 3 other files 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.MySQL;
using ArxSamples.MySQL.Operations.Create.Basics;
// The connection string comes from the environment, never from the code (see "Configure the connection").
var connectionString = Environment.GetEnvironmentVariable("ARX_SAMPLES_MYSQL")
?? throw new InvalidOperationException("Set the ARX_SAMPLES_MYSQL environment variable to your MySQL connection string.");
await new Basic3_CatchAnError().RunAsync(new SampleContext(connectionString));
The sample, as it is in the repository.
using ARX.SDK;
using ARX.SDK.MySQL.Services;
using ARX.SDK.Services;
using static ArxSamples.MySQL.SampleOutput;
namespace ArxSamples.MySQL.Operations.Create.Basics;
/// <summary>
/// Catching errors, in three scenarios: a database error by its code, several database errors by their category, and a
/// mistake ARX refuses before anything is sent to the database.
/// </summary>
public sealed class Basic3_CatchAnError : ISample
{
public string Name => "create-basics-3-catch-an-error";
public string Title => "Catch an error by its code";
public async Task RunAsync(SampleContext dbContext)
{
await Scenario1_ADatabaseErrorByItsCodeAsync(dbContext.ConnectionString);
await Scenario2_DatabaseErrorsByTheirCategoryAsync(dbContext.ConnectionString);
await Scenario3_AMistakeRefusedBeforeTheDatabaseAsync(dbContext.ConnectionString);
}
private static ArxMySqlDbService Service(string connectionString) => new ArxMySqlDbService(connectionString, ArxSchemaCatalog.Create()
.Table("account", ArxIdColumn.Generated<long>("id"))
.Table("country", ArxIdColumn.Supplied<string>("code"))
.Build());
/// <summary>1. The usual way: the database refuses a record, and the ARX code says why. Here, a duplicate account number.</summary>
private static async Task Scenario1_ADatabaseErrorByItsCodeAsync(string connectionString)
{
Step("1. By its code");
var service = Service(connectionString);
var original = new ArxEntity("account");
original["name"] = "Tailspin Toys";
original["account_number"] = "ACC-0005";
await service.CreateAsync(original);
// Account numbers are unique, so the database refuses this one.
var duplicate = new ArxEntity("account");
duplicate["name"] = "Tailspin Toys (copy)";
duplicate["account_number"] = "ACC-0005";
try
{
await service.CreateAsync(duplicate);
}
catch (ArxDatabaseException error) when (error.Code == ArxErrorCode.UniqueViolation)
{
Show($"not created: {error.Message}");
}
/* ===========================================================
* [DIRECT QUERY CHECK]
* -----------------------------------------------------------
* SELECT name, account_number
* FROM account
* WHERE account_number = 'ACC-0005';
* -- Expect: 1 row, Tailspin Toys: the copy was refused.
* ===========================================================
*/
}
/// <summary>
/// 2. The other way: catch by category when several codes need the same handling. Every constraint the database
/// enforces (unique, not null, check, foreign key) is the Constraint category.
/// </summary>
private static async Task Scenario2_DatabaseErrorsByTheirCategoryAsync(string connectionString)
{
Step("2. By its category");
var service = Service(connectionString);
var numberTaken = new ArxEntity("account");
numberTaken["name"] = "Tailspin Toys (another copy)";
numberTaken["account_number"] = "ACC-0005";
var noName = new ArxEntity("account");
noName["account_number"] = "ACC-0013"; // name is required (NOT NULL)
foreach (var account in new[] { numberTaken, noName })
{
try
{
await service.CreateAsync(account);
}
catch (ArxDatabaseException error) when (error.Category == ArxErrorCategory.Constraint)
{
Show($"{error.Category}: {error.Code}");
}
}
/* ===========================================================
* [DIRECT QUERY CHECK]
* -----------------------------------------------------------
* SELECT account_number, count(*)
* FROM account
* WHERE account_number IN ('ACC-0005', 'ACC-0013')
* GROUP BY account_number;
* -- Expect: only ACC-0005, count 1: neither refused account was stored.
* ===========================================================
*/
}
/// <summary>
/// 3. A mistake in the request itself is refused by ARX before anything is sent: an ArxValidationException, not an
/// ArxDatabaseException. Here, a country without its code, which the schema declares as supplied.
/// </summary>
private static async Task Scenario3_AMistakeRefusedBeforeTheDatabaseAsync(string connectionString)
{
Step("3. Before the database");
var service = Service(connectionString);
var noCode = new ArxEntity("country");
noCode["name"] = "Narnia";
try
{
await service.CreateAsync(noCode);
}
catch (ArxValidationException error) when (error.Code == ArxErrorCode.InvalidArgument)
{
Show($"{error.Code}: {error.Message}");
}
/* ===========================================================
* [DIRECT QUERY CHECK]
* -----------------------------------------------------------
* SELECT count(*)
* FROM country
* WHERE name = 'Narnia';
* -- Expect: 0: refused before anything was sent.
* ===========================================================
*/
}
}
What every sample is given: the connection string (and the ISample interface the samples implement).
namespace ArxSamples.MySQL;
/// <summary>One documentation sample: a scenario a real application has, shown end to end.</summary>
public interface ISample
{
/// <summary>The name to run it alone: <c>dotnet run -- <name></c>.</summary>
string Name { get; }
/// <summary>One line on what it shows.</summary>
string Title { get; }
Task RunAsync(SampleContext dbContext);
}
/// <summary>
/// What a sample is given. Every sample gets the connection string; the application's service and catalog
/// (SampleContext.Application.cs) and the plain MySqlConnector read-back helper (SampleContext.Direct.cs) live in their own
/// files, so a sample needs only the files it uses.
/// </summary>
public sealed partial class SampleContext
{
public SampleContext(string connectionString)
{
ConnectionString = connectionString;
}
public string ConnectionString { get; }
}
How the samples print: Step for each scenario, Show for what it did, ShowError for an error caught on purpose.
using ARX.SDK;
namespace ArxSamples.MySQL;
/// <summary>How the samples print: a numbered step for each scenario, then what it did.</summary>
public static class SampleOutput
{
/// <summary>The start of a scenario: <c>Step("1. By the id Create returned")</c>.</summary>
public static void Step(string text) => Console.WriteLine($" > {text}");
/// <summary>What the scenario did.</summary>
public static void Show(string text) => Console.WriteLine($" {text}");
/// <summary>An ARX error the scenario caught on purpose.</summary>
public static void ShowError(ArxException exception)
=> Console.WriteLine($" caught {exception.GetType().Name}: {exception.Message}");
}
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. By its code
not created: ARX3001 UniqueViolation: Create on 'account' violates unique key 'account_account_number_key'.
> 2. By its category
Constraint: UniqueViolation
Constraint: NotNullViolation
> 3. Before the database
InvalidArgument: ARX1001 InvalidArgument: Id column 'code' of 'country' takes its value from the caller (declared, or read from the table, as supplied), but the entity has no attribute for it. Set it (entity["code"] = Guid.NewGuid()): MySQL doesn't return a text or Guid id it makes itself, even with DEFAULT (UUID()), so ARX can't read one back.