MongoDB · Delete
Replace a record in a transaction
Replace an order in one ARX transaction: delete the old one, create the new one, commit, in two scenarios: the replacement, and a replacement that fails half way and leaves the old order in place.
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); SampleContext.Application.cs (dbContext.Service, the application's ARX service); SampleCatalog.cs (the tables' ids, keys, lookups and display names); SampleContext.Direct.cs (dbContext.Direct); MongoDirectHelper.cs (the checks behind dbContext.Direct), in the files below.
dbContext.Direct is the samples' own checking code on the plain driver, not part of ARX: leave it out of your application.
Before scenario 1 the sample runs this, so the service and the rows the scenarios use are there:
var order = new ArxEntity("sales_order");
order["order_number"] = "SO-M931";
order["total"] = 40m;
var oldId = await service.CreateAsync(order);1 Replace
Delete SO-M931 and create SO-M932 in its place, committed together.
Already in scope: service (ArxMongoDbService), oldId (ArxRecordId)
await using var connection = new ArxMongoConnection(service);
await using var transaction = await connection.BeginTransactionAsync();
await service.DeleteAsync("sales_order", oldId, transaction);
var replacement = new ArxEntity("sales_order");
replacement["order_number"] = "SO-M932";
replacement["total"] = 45m;
await service.CreateAsync(replacement, transaction);
await transaction.CommitAsync();
Console.WriteLine("SO-M931 replaced by SO-M932");Check it in the database
Run it in mongosh connected to the database, after the code has run.
db.sales_order.find({ order_number: { $in: ["SO-M931", "SO-M932"] } }, { order_number: 1 })Expect: 1 document, SO-M932.
2 A failed replacement
The new order repeats an existing number: the transaction is rolled back, SO-M932 stays.
Already in scope: service (ArxMongoDbService)
await using var connection = new ArxMongoConnection(service);
await using var transaction = await connection.BeginTransactionAsync();
var match = new ArxEntity("sales_order");
match["order_number"] = "SO-M932";
await service.DeleteAsync(match, transaction, new ArxDeleteOptions { Match = ArxMatch.Key("order_number") });
var first = new ArxEntity("sales_order");
first["order_number"] = "SO-M933";
var second = new ArxEntity("sales_order");
second["order_number"] = "SO-M933";
try
{
await service.CreateMultipleAsync(new[] { first, second }, transaction);
await transaction.CommitAsync();
}
catch (ArxDatabaseException exception)
{
Console.WriteLine(exception.Message);
await transaction.RollbackAsync();
}Check it in the database
Run it in mongosh connected to the database, after the code has run.
db.sales_order.find({ order_number: { $in: ["SO-M932", "SO-M933"] } }, { order_number: 1 })Expect: 1 document, SO-M932: the delete was rolled back with the rest.
Good to know
MongoDB can't change a document's _id, so a new id means a new document: delete and create, in one transaction, so a reader never sees neither or both.
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.MongoDb --version 1.0.0-rc.1 - Database: a MongoDB 7.0, a replica set (tested with 7.0.45, one node) database with the sample schema.
- Configuration: set
ARX_SAMPLES_MONGODBto its connection string. Nothing is hard-coded. - Source code: replace
Program.csand add the 7 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 and first remakes the sample database from sample-schema.json.
using ArxSamples.MongoDb;
using ArxSamples.MongoDb.Operations.Delete.AdvancedCases;
// The connection string comes from the environment, never from the code (see "Configure the connection").
var connectionString = Environment.GetEnvironmentVariable("ARX_SAMPLES_MONGODB")
?? throw new InvalidOperationException("Set the ARX_SAMPLES_MONGODB environment variable to your MongoDB connection string.");
var dbContext = new SampleContext(connectionString);
// Every run starts from the sample data: this deletes and makes again what sample-schema.json lists
// (save the file in the project folder, see Get started).
await dbContext.Direct.ResetAsync();
await new Advanced1_ReplaceAnOrder().RunAsync(dbContext);
The sample, as it is in the repository.
using ARX.SDK;
using ARX.SDK.MongoDb.Services;
using ARX.SDK.Services;
using static ArxSamples.MongoDb.SampleOutput;
namespace ArxSamples.MongoDb.Operations.Delete.AdvancedCases;
/// <summary>
/// Replace an order in one ARX transaction: delete the old one, create the new one, commit, in two scenarios: the
/// replacement, and a replacement that fails half way and leaves the old order in place.
/// </summary>
/// <remarks>
/// MongoDB can't change a document's <c>_id</c>, so a new id means a new document: delete and create, in one transaction,
/// so a reader never sees neither or both.
/// </remarks>
public sealed class Advanced1_ReplaceAnOrder : ISample
{
public string Name => "delete-advanced-1-replace-an-order";
public string Title => "Replace a record in a transaction";
public async Task RunAsync(SampleContext dbContext)
{
var order = new ArxEntity("sales_order");
order["order_number"] = "SO-M931";
order["total"] = 40m;
var oldId = await dbContext.Service.CreateAsync(order);
await Scenario1_ReplaceAsync(dbContext.Service, oldId);
await Scenario2_AFailedReplacementAsync(dbContext.Service);
}
/// <summary>1. Delete SO-M931 and create SO-M932 in its place, committed together.</summary>
private static async Task Scenario1_ReplaceAsync(ArxMongoDbService service, ArxRecordId oldId)
{
Step("1. Replace");
await using var connection = new ArxMongoConnection(service);
await using var transaction = await connection.BeginTransactionAsync();
await service.DeleteAsync("sales_order", oldId, transaction);
var replacement = new ArxEntity("sales_order");
replacement["order_number"] = "SO-M932";
replacement["total"] = 45m;
await service.CreateAsync(replacement, transaction);
await transaction.CommitAsync();
Show("SO-M931 replaced by SO-M932");
/* ===========================================================
* [DIRECT QUERY CHECK]
* -----------------------------------------------------------
* db.sales_order.find({ order_number: { $in: ["SO-M931", "SO-M932"] } }, { order_number: 1 })
* -- Expect: 1 document, SO-M932.
* ===========================================================
*/
}
/// <summary>2. The new order repeats an existing number: the transaction is rolled back, SO-M932 stays.</summary>
private static async Task Scenario2_AFailedReplacementAsync(ArxMongoDbService service)
{
Step("2. A failed replacement");
await using var connection = new ArxMongoConnection(service);
await using var transaction = await connection.BeginTransactionAsync();
var match = new ArxEntity("sales_order");
match["order_number"] = "SO-M932";
await service.DeleteAsync(match, transaction, new ArxDeleteOptions { Match = ArxMatch.Key("order_number") });
var first = new ArxEntity("sales_order");
first["order_number"] = "SO-M933";
var second = new ArxEntity("sales_order");
second["order_number"] = "SO-M933";
try
{
await service.CreateMultipleAsync(new[] { first, second }, transaction);
await transaction.CommitAsync();
}
catch (ArxDatabaseException exception)
{
ShowError(exception);
await transaction.RollbackAsync();
}
/* ===========================================================
* [DIRECT QUERY CHECK]
* -----------------------------------------------------------
* db.sales_order.find({ order_number: { $in: ["SO-M932", "SO-M933"] } }, { order_number: 1 })
* -- Expect: 1 document, SO-M932: the delete was rolled back with the rest.
* ===========================================================
*/
}
}
What every sample is given: the connection string (and the ISample interface the samples implement).
namespace ArxSamples.MongoDb;
/// <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 MongoDB driver 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.MongoDb;
/// <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}");
}
The application's ARX service and its schema catalog, created once and shared by the samples.
using ARX.SDK.MongoDb.Services;
using ARX.SDK.Services;
namespace ArxSamples.MongoDb;
/// <summary>The application's ARX service and its schema catalog, shared by the real and advanced samples.</summary>
public sealed partial class SampleContext
{
private ArxSchemaCatalog? _catalog;
private ArxMongoDbService? _service;
/// <summary>The application's schema catalog: collection ids, keys, lookups and selection sets (SampleCatalog.cs).</summary>
public ArxSchemaCatalog Catalog => _catalog ??= SampleCatalog.Build();
/// <summary>
/// One service for the whole application. It shares one MongoDB client per connection string (a client is a
/// connection pool), so make it once and keep it.
/// </summary>
public ArxMongoDbService Service => _service ??= new ArxMongoDbService(ConnectionString, Catalog);
}
The application's schema catalog: every table's id column and keys, and its selection sets.
using ARX.SDK.Services;
namespace ArxSamples.MongoDb;
/// <summary>An account's industry. Stored in account.industry as the enum's number.</summary>
public enum Industry
{
Retail = 1,
Manufacturing = 2,
Healthcare = 3,
Finance = 4,
Technology = 5
}
/// <summary>
/// The application's schema catalog: every collection's id and keys, the lookups between collections, and every
/// picklist (selection set), defined once.
/// </summary>
/// <remarks>
/// <para>
/// <b>Ids.</b> On MongoDB the declared id is the document's <c>_id</c>, named in your code by the column you declare
/// (<c>account["account_id"]</c>). A generated string id is an ObjectId ARX makes before the insert and gives back as its
/// 24-character text; a generated Guid is one ARX makes; a supplied id is yours (a country code, a SKU). MongoDB has no
/// sequences, so a generated number isn't offered, and every document has an <c>_id</c>, so there is no NoIdColumn.
/// </para>
/// <para>
/// <b>Keys.</b> Business keys (an account number; a region and a SKU together) let Update, Delete and Retrieve find
/// documents by them. MongoDB enforces them with unique indexes: the sample database has one for each.
/// </para>
/// <para>
/// <b>Lookups.</b> MongoDB has no foreign keys, so the catalog says which field refers to which collection
/// (<c>.Lookup("contact", "account_id", "account")</c>). ARX stores the referenced id as it is stored there (an ObjectId)
/// and reads the field back as an <see cref="ArxEntityReference"/> with the referenced record's display name.
/// </para>
/// <para>
/// <b>Selection sets.</b> A field that only accepts a fixed list of codes: an account's status, a contact's preferred
/// channels (several codes, stored as an array). ARX checks every <see cref="ArxSelectionSetValue"/> against the catalog
/// before anything is sent and stores the codes themselves.
/// </para>
/// </remarks>
public static class SampleCatalog
{
public static ArxSchemaCatalog Build() => ArxSchemaCatalog.Create()
.Table("account", ArxIdColumn.Generated<string>("account_id"))
.Key("account", "account_number")
.DisplayName("account", "name")
.Table("contact", ArxIdColumn.Generated<string>("contact_id"))
.Key("contact", "email")
.Lookup("contact", "account_id", "account")
.DisplayName("contact", new[] { "first_name", "last_name" }, " ")
.Table("country", ArxIdColumn.Supplied<string>("code"))
.DisplayName("country", "name")
.Table("product", ArxIdColumn.Supplied<string>("sku"))
.DisplayName("product", "name")
.Table("price_list", ArxIdColumn.Generated<string>("price_id"))
.Key("price_list", "region", "sku")
.Lookup("price_list", "region", "country")
.Table("sales_order", ArxIdColumn.Generated<Guid>("order_id"))
.Key("sales_order", "order_number")
.Lookup("sales_order", "account_id", "account")
.Table("activity_log", ArxIdColumn.Generated<string>("log_id"))
.AddEnum<Industry>("Industry")
.Add("AccountStatus", ArxSelectionSetKind.String, ("ACTIVE", "Active"), ("ON_HOLD", "On hold"), ("CLOSED", "Closed"))
.Add("ContactChannels", ArxSelectionSetKind.String, ArxSelectionSetMultiplicity.Array, ("E", "Email"), ("P", "Phone"), ("S", "SMS"))
.Build();
}
The plain MongoDB.Driver read-back helper, for the samples that show what MongoDB stored.
namespace ArxSamples.MongoDb;
/// <summary>The plain MongoDB driver outside the SDK, for the samples that show what MongoDB stored.</summary>
public sealed partial class SampleContext
{
private MongoDirectHelper? _direct;
/// <summary>The plain MongoDB driver outside the SDK, used only to reset the database and to show what was stored.</summary>
public MongoDirectHelper Direct => _direct ??= new MongoDirectHelper(ConnectionString);
}
Plain MongoDB.Driver, not part of ARX: reads rows back so a sample can show what MongoDB stored.
using MongoDB.Bson;
using MongoDB.Driver;
namespace ArxSamples.MongoDb;
/// <summary>
/// The plain MongoDB driver, outside the SDK: it resets the sample database from Schema/sample-schema.json and shows
/// what was stored. Samples never write through it.
/// </summary>
public sealed class MongoDirectHelper
{
private readonly IMongoDatabase _database;
public MongoDirectHelper(string connectionString)
{
var url = MongoUrl.Create(connectionString);
_database = new MongoClient(url).GetDatabase(url.DatabaseName ?? "arx_samples");
}
/// <summary>
/// The sample schema: Schema/sample-schema.json next to the program (the samples' own project), or sample-schema.json
/// in the project folder (a sample copied from its page, as Get started says).
/// </summary>
private static string SchemaPath()
{
var candidates = new[]
{
Path.Combine(AppContext.BaseDirectory, "Schema", "sample-schema.json"),
Path.Combine(Directory.GetCurrentDirectory(), "sample-schema.json"),
Path.Combine(Directory.GetCurrentDirectory(), "Schema", "sample-schema.json")
};
return candidates.FirstOrDefault(File.Exists)
?? throw new FileNotFoundException("sample-schema.json isn't in the project folder: download it from the Get started page and save it next to Program.cs.");
}
/// <summary>Drops the sample database and makes its collections, indexes and validators again.</summary>
public async Task ResetAsync()
{
var schema = BsonDocument.Parse(await File.ReadAllTextAsync(SchemaPath()));
foreach (var collection in schema["collections"].AsBsonArray.Select(x => x.AsBsonDocument))
{
var name = collection["name"].AsString;
// Only the collections the schema lists are dropped and made again: anything else in the database is left alone.
await _database.DropCollectionAsync(name);
var options = new CreateCollectionOptions<BsonDocument>();
if (collection.TryGetValue("validator", out var validator))
{
options.Validator = validator.AsBsonDocument;
}
await _database.CreateCollectionAsync(name, options);
foreach (var index in collection.GetValue("indexes", new BsonArray()).AsBsonArray.Select(x => x.AsBsonDocument))
{
var keys = index["keys"].AsBsonDocument;
var indexOptions = new CreateIndexOptions<BsonDocument> { Unique = index.GetValue("unique", false).AsBoolean };
if (index.GetValue("partial", false).AsBoolean)
{
indexOptions.PartialFilterExpression = new BsonDocument(keys.Names.Select(x => new BsonElement(x, new BsonDocument("$exists", true))));
}
await _database.GetCollection<BsonDocument>(name).Indexes.CreateOneAsync(new CreateIndexModel<BsonDocument>(keys, indexOptions));
}
}
}
/// <summary>The stored documents a filter finds, as MongoDB's JSON, one line each.</summary>
public async Task<IReadOnlyList<string>> FindAsync(string collection, string filterJson)
{
var documents = await _database.GetCollection<BsonDocument>(collection).Find(BsonDocument.Parse(filterJson)).ToListAsync();
return documents.Select(x => x.ToJson()).ToList();
}
}
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. Replace
SO-M931 replaced by SO-M932
> 2. A failed replacement
caught ArxDatabaseException: ARX3001 UniqueViolation: CreateMultiple on 'sales_order': a record with the same value in unique index 'order_number_1' already exists.