SQL Server
Get started
Everything the samples need on your machine, once: the .NET SDK, a project, the packages, the database and the connection string.
Install the .NET SDK
.NET SDK 10.0.100 or later 10.0.x. The samples target
net10.0. Check what you have:Terminal dotnet --versionCreate a project
Terminal dotnet new console -n ArxSample -f net10.0 cd ArxSampleAdd the packages
ARX.SDK.SQL1.0.0-rc.1: The SQL Server provider. It brings ARX.SDK and Microsoft.Data.SqlClient 7.1.1 with it.ARX.SDK1.0.0-rc.1: The core: entities, queries, the schema catalog, errors. Comes with the provider.Microsoft.Data.SqlClient7.1.1: The SQL Server driver. Comes with the provider.
Terminal dotnet add package ARX.SDK.SQL --version 1.0.0-rc.1Some advanced samples need one more package; their pages give the exact command.
Create the database and the sample schema
SQL Server 2022 (tested with 16.0.4225.2), through Microsoft.Data.SqlClient 7.1.1 or later (the lowest version, which the provider is built on; Versions and compatibility lists the versions it is tested on). With Docker, start one and create the database (choose your own password):
Terminal docker run --name arx-samples-mssql -e ACCEPT_EULA=Y -e MSSQL_SA_PASSWORD=Your-password1 -p 1433:1433 -d mcr.microsoft.com/mssql/server:2022-latest docker exec arx-samples-mssql /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P Your-password1 -C -Q "CREATE DATABASE arx_samples; ALTER DATABASE arx_samples SET ALLOW_SNAPSHOT_ISOLATION ON"Then create the sample tables: download
sample-schema.sql, or copy it below, and run it. Run it again whenever you want to start over: it drops and recreates the sample tables, empty.Terminal macOS and Linux (bash, zsh)
docker exec -i arx-samples-mssql /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P Your-password1 -C -I -b -d arx_samples < sample-schema.sqlPowerShell Windows PowerShell
Get-Content sample-schema.sql -Raw | docker exec -i arx-samples-mssql /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P Your-password1 -C -I -b -d arx_samplessample-schema.sql The sample schema the samples run against.
-- Schema for the ARX.SDK SQL Server documentation samples: a small CRM and product catalogue, and a table of every -- data type. Run it in a database of its own (arx_samples). Re-runnable: it drops, by name, only the tables, views and -- routines it creates below, and the reporting schema once nothing else is left in it, so running it again starts over -- and nothing else in the database is touched. Batches are separated by GO lines (a tool's separator, not T-SQL). Run it with sqlcmd or any SQL tool against the -- samples' database, for example: -- sqlcmd -S localhost,11433 -U sa -d arx_samples -C -i sample-schema.sql -- -- ARX conventions used here: the table name is the entity name, and a lookup column stores the referenced row's id. -- Every table's id column is declared in SampleCatalog.cs: most are a generated bigint "id"; country and device show a -- natural id and a generated uniqueidentifier; price_list and activity_log have no id column (NoIdColumn). Selection -- set columns hold the option's code itself. -- The routines and views this script creates, and no other: only these names, only in dbo and reporting. DECLARE @drop nvarchar(max) = ( SELECT STRING_AGG(CONCAT(CASE o.type WHEN 'P' THEN N'DROP PROCEDURE ' WHEN 'V' THEN N'DROP VIEW ' ELSE N'DROP FUNCTION ' END, QUOTENAME(SCHEMA_NAME(o.schema_id)), N'.', QUOTENAME(o.name)), N'; ') FROM sys.objects o WHERE o.type IN ('P', 'FN', 'IF', 'TF', 'V') AND SCHEMA_NAME(o.schema_id) IN ('dbo', 'reporting') AND o.name IN ( N'fn_account_totals', N'fn_accounts_with_status', N'fn_contact_count', N'fn_contacts_named', N'fn_contacts_of_account', N'fn_order_count', N'fn_products_like', N'sp_account_report', N'sp_account_totals', N'sp_import_progress', N'sp_order_with_lines', N'sp_place_order', N'sp_products', N'sp_products_page', N'sp_recalculate_order_totals', N'sp_recalculate_totals', N'sp_rename_account', N'sp_reserve_credit', N'sp_slow', N'vw_account_overview', N'vw_account_sales_summary', N'vw_contact_directory', N'vw_key_accounts', N'vw_product_catalogue')); IF @drop IS NOT NULL EXEC (@drop); GO IF OBJECT_ID('account') IS NOT NULL ALTER TABLE account DROP CONSTRAINT IF EXISTS account_primary_contact_fkey; DROP TABLE IF EXISTS legacy_customer, web_visit, activity_log, price_list, device, country, order_line, sales_order, data_type_showcase, product, product_category, contact, account; -- The reporting schema is the script's own: it goes only when nothing else is left in it. IF SCHEMA_ID('reporting') IS NOT NULL AND NOT EXISTS (SELECT 1 FROM sys.objects WHERE schema_id = SCHEMA_ID('reporting')) EXEC ('DROP SCHEMA reporting'); GO -- Customers. industry is an Integer selection set built from a .NET enum; status a String set the CHECK also guards; -- segments a multiple-choice String set stored as delimited text (SQL Server has no arrays). CREATE TABLE account ( id bigint IDENTITY(1, 1) PRIMARY KEY, guid uniqueidentifier NOT NULL DEFAULT NEWID(), name nvarchar(200) NOT NULL, account_number varchar(20) NULL, industry smallint, status varchar(10) NOT NULL DEFAULT 'ACTIVE' CONSTRAINT account_status_check CHECK (status IN ('ACTIVE', 'ON_HOLD', 'CLOSED')), segments nvarchar(200), website varchar(200), annual_revenue decimal(18, 2), employee_count int CONSTRAINT account_employee_count_check CHECK (employee_count >= 0), credit_limit decimal(18, 2), is_key_account bit NOT NULL DEFAULT 0, founded_on date, parent_account_id bigint REFERENCES account (id), primary_contact_id bigint, -- references contact (added after contact below; the preload samples follow it) created_on datetimeoffset NOT NULL DEFAULT SYSDATETIMEOFFSET(), modified_on datetimeoffset ); GO -- A unique account number that several accounts may leave empty (a UNIQUE constraint allows one NULL only). CREATE UNIQUE INDEX account_account_number_key ON account (account_number) WHERE account_number IS NOT NULL; GO -- People at a customer. account_id is a lookup (ArxEntityReference); email is unique. preferred_channel, lead_source -- and rating are selection sets (lead_source and rating come from the catalog's JSON). CREATE TABLE contact ( id bigint IDENTITY(1, 1) PRIMARY KEY, guid uniqueidentifier NOT NULL DEFAULT NEWID(), account_id bigint REFERENCES account (id), first_name nvarchar(100) NOT NULL, last_name nvarchar(100) NOT NULL, job_title nvarchar(100), email varchar(254), phone varchar(30), preferred_channel char(1), lead_source nvarchar(50), rating smallint, birthday date, do_not_email bit NOT NULL DEFAULT 0, consent_given_at datetimeoffset, notes nvarchar(max), created_on datetimeoffset NOT NULL DEFAULT SYSDATETIMEOFFSET() ); GO CREATE UNIQUE INDEX contact_email_key ON contact (email) WHERE email IS NOT NULL; -- account.primary_contact_id: a contact of the account, the reference a preload follows to add contact. ALTER TABLE account ADD CONSTRAINT account_primary_contact_fkey FOREIGN KEY (primary_contact_id) REFERENCES contact (id); CREATE TABLE product_category ( id bigint IDENTITY(1, 1) PRIMARY KEY, name nvarchar(100) NOT NULL CONSTRAINT product_category_name_key UNIQUE ); -- The catalogue. attributes is JSON text (ArxJsonB; no JSON type in SQL Server 2022, ISJSON checks it); image is binary. -- tags is delimited text. CREATE TABLE product ( id bigint IDENTITY(1, 1) PRIMARY KEY, guid uniqueidentifier NOT NULL DEFAULT NEWID(), sku varchar(32) NOT NULL CONSTRAINT product_sku_key UNIQUE, name nvarchar(200) NOT NULL, description nvarchar(max), category_id bigint REFERENCES product_category (id), unit_price decimal(12, 2) NOT NULL CONSTRAINT product_unit_price_check CHECK (unit_price >= 0), cost decimal(12, 4), weight_kg float, stock_quantity int NOT NULL DEFAULT 0, is_active bit NOT NULL DEFAULT 1, tags nvarchar(400), attributes nvarchar(max) CONSTRAINT product_attributes_check CHECK (attributes IS NULL OR ISJSON(attributes) = 1), image varbinary(max), launched_on date, created_on datetimeoffset NOT NULL DEFAULT SYSDATETIMEOFFSET() ); -- An order and its lines, for the "one process, one transaction" samples. CREATE TABLE sales_order ( id bigint IDENTITY(1, 1) PRIMARY KEY, order_number varchar(20) NOT NULL CONSTRAINT sales_order_order_number_key UNIQUE, account_id bigint NOT NULL REFERENCES account (id), ordered_on datetimeoffset NOT NULL, total decimal(14, 2) NOT NULL DEFAULT 0 ); CREATE TABLE order_line ( id bigint IDENTITY(1, 1) PRIMARY KEY, order_id bigint NOT NULL REFERENCES sales_order (id), product_id bigint NOT NULL REFERENCES product (id), quantity int NOT NULL CONSTRAINT order_line_quantity_check CHECK (quantity > 0), unit_price decimal(12, 2) NOT NULL ); -- One column for every kind of value the SQL Server provider writes (README, "Type mapping"). CREATE TABLE data_type_showcase ( id bigint IDENTITY(1, 1) PRIMARY KEY, text_value nvarchar(max), varchar_value varchar(50), char_value nchar(3), tinyint_value tinyint, smallint_value smallint, int_value int, bigint_value bigint, decimal_value decimal(30, 6), money_value money, real_value real, float_value float, bit_value bit, uniqueidentifier_value uniqueidentifier, datetimeoffset_value datetimeoffset, datetime2_value datetime2, datetime_value datetime, date_value date, time_value time, varbinary_value varbinary(max), json_value nvarchar(max) CONSTRAINT data_type_showcase_json_check CHECK (json_value IS NULL OR ISJSON(json_value) = 1), mood nvarchar(10) CONSTRAINT data_type_showcase_mood_check CHECK (mood IN (N'happy', N'neutral', N'sad')), account_id bigint REFERENCES account (id), industry smallint ); -- Ids that aren't a generated bigint: a natural text id and a generated uniqueidentifier. Then two tables with no id -- column: a price identified by region and SKU together, and a log whose rows may repeat. All declared in -- SampleCatalog.cs: ARX reads ids and keys from the database only when asked to preload them. CREATE TABLE country ( code char(2) PRIMARY KEY, name nvarchar(100) NOT NULL ); CREATE TABLE device ( id uniqueidentifier PRIMARY KEY DEFAULT NEWID(), serial_number varchar(50) NOT NULL CONSTRAINT device_serial_number_key UNIQUE, account_id bigint REFERENCES account (id) ); CREATE TABLE price_list ( region char(2) NOT NULL REFERENCES country (code), sku varchar(32) NOT NULL, unit_price decimal(12, 2) NOT NULL CONSTRAINT price_list_unit_price_check CHECK (unit_price >= 0), valid_from date NOT NULL, PRIMARY KEY (region, sku) ); -- No primary key at all: the same action can be logged twice. CREATE TABLE activity_log ( account_id bigint REFERENCES account (id), action nvarchar(200) NOT NULL, logged_at datetimeoffset NOT NULL DEFAULT SYSDATETIMEOFFSET() ); -- A table made outside ARX, its columns named outside ARX's name rule: used through aliases (Create Advanced10). CREATE TABLE legacy_customer ( [Customer Code] nvarchar(20) NOT NULL PRIMARY KEY, [first-name] nvarchar(100) NOT NULL, [last-name] nvarchar(100) NULL, [order.total] decimal(12, 2) NULL ); -- Web visits (declared NoIdColumn in SampleCatalog.cs). CREATE TABLE web_visit ( account_id bigint REFERENCES account (id), page nvarchar(200) NOT NULL, visited_on date NOT NULL ); GO -- Naming: views vw_, functions fn_, procedures sp_; tables have no prefix. The name says what an object is where only -- the name is visible (a query, a log, an error message), and ARX reads them all by name. -- Retrieve: a view an application reads like a table, and a function that returns a table. CREATE VIEW vw_account_overview AS SELECT a.id, a.name, a.account_number, a.status, COUNT_BIG(c.id) AS contacts FROM account a LEFT JOIN contact c ON c.account_id = a.id GROUP BY a.id, a.name, a.account_number, a.status; GO CREATE FUNCTION fn_contacts_of_account(@p_account_id bigint) RETURNS TABLE AS RETURN SELECT id, first_name, last_name, email FROM contact WHERE account_id = @p_account_id; GO -- A view in another schema: read as "reporting.vw_product_catalogue". IF SCHEMA_ID('reporting') IS NULL EXEC ('CREATE SCHEMA reporting'); GO CREATE VIEW reporting.vw_product_catalogue AS SELECT p.id, p.sku, p.name, p.unit_price, c.name AS category FROM product p LEFT JOIN product_category c ON c.id = p.category_id WHERE p.is_active = 1; GO -- An account summary, computed at each read (an indexed view would store it instead). CREATE VIEW vw_account_sales_summary AS SELECT a.id AS account_id, a.name, a.account_number, COUNT_BIG(o.id) AS orders, COALESCE(SUM(o.total), 0) AS revenue FROM account a LEFT JOIN sales_order o ON o.account_id = a.id GROUP BY a.id, a.name, a.account_number; GO -- Function shapes: a table's rows, one row of totals (a multi-statement function), one value (a scalar function). CREATE FUNCTION fn_accounts_with_status(@p_status varchar(10)) RETURNS TABLE AS RETURN SELECT * FROM account WHERE status = @p_status; GO CREATE FUNCTION fn_account_totals() RETURNS @totals TABLE (accounts bigint, contacts bigint) AS BEGIN INSERT INTO @totals SELECT (SELECT COUNT_BIG(*) FROM account), (SELECT COUNT_BIG(*) FROM contact); RETURN; END; GO CREATE FUNCTION fn_order_count(@p_account_id bigint) RETURNS bigint AS BEGIN RETURN (SELECT COUNT_BIG(*) FROM sales_order WHERE account_id = @p_account_id); END; GO -- A procedure: not a row source (Execute's, not Retrieve's). CREATE PROCEDURE sp_recalculate_order_totals AS UPDATE o SET total = COALESCE((SELECT SUM(l.quantity * l.unit_price) FROM order_line l WHERE l.order_id = o.id), 0) FROM sales_order o; GO -- Views in depth: a directory of contacts without their private columns (email, phone, birthday, notes), and a view -- built on another view. CREATE VIEW vw_contact_directory AS SELECT id, first_name, last_name, job_title, account_id FROM contact; GO CREATE VIEW vw_key_accounts AS SELECT id, name, account_number, contacts FROM vw_account_overview WHERE contacts >= 2; GO -- Execute: procedures (sp_) and functions (fn_) the samples call. -- Rename an account (no result); its totals for a year (IN, IN/OUT and OUTPUT values). CREATE PROCEDURE sp_rename_account @p_account_id bigint, @p_name nvarchar(200), @p_reason nvarchar(200) = N'not given' AS UPDATE account SET name = @p_name, modified_on = SYSDATETIMEOFFSET() WHERE id = @p_account_id; GO CREATE PROCEDURE sp_account_totals @p_account_id bigint, @p_year int OUTPUT, @p_orders bigint OUTPUT, @p_revenue decimal(18, 2) OUTPUT AS BEGIN SET NOCOUNT ON; SET @p_year = COALESCE(@p_year, YEAR(SYSDATETIMEOFFSET())); SELECT @p_orders = COUNT_BIG(*), @p_revenue = COALESCE(SUM(total), 0) FROM sales_order WHERE account_id = @p_account_id AND YEAR(ordered_on) = @p_year; END; GO -- A value; rows. CREATE FUNCTION fn_contact_count(@p_account_id bigint) RETURNS bigint AS BEGIN RETURN (SELECT COUNT_BIG(*) FROM contact WHERE account_id = @p_account_id); END; GO CREATE FUNCTION fn_contacts_named(@p_last_name nvarchar(100)) RETURNS TABLE AS RETURN SELECT id, first_name, job_title FROM contact WHERE last_name = @p_last_name; GO -- Place an order: writes sales_order and order_line, returns the new id and the total. The SKUs are comma-separated. CREATE PROCEDURE sp_place_order @p_account_id bigint, @p_order_number varchar(20), @p_skus nvarchar(max), @p_quantity int, @p_order_id bigint OUTPUT, @p_total decimal(14, 2) OUTPUT AS BEGIN SET NOCOUNT ON; INSERT INTO sales_order (order_number, account_id, ordered_on) VALUES (@p_order_number, @p_account_id, SYSDATETIMEOFFSET()); SET @p_order_id = SCOPE_IDENTITY(); INSERT INTO order_line (order_id, product_id, quantity, unit_price) SELECT @p_order_id, p.id, @p_quantity, p.unit_price FROM product p WHERE p.sku IN (SELECT TRIM(value) FROM STRING_SPLIT(@p_skus, ',')); SET @p_total = (SELECT COALESCE(SUM(quantity * unit_price), 0) FROM order_line WHERE order_id = @p_order_id); UPDATE sales_order SET total = @p_total WHERE id = @p_order_id; END; GO -- Tables from a procedure: an order and its lines; an account report with three tables (result sets, in order). CREATE PROCEDURE sp_order_with_lines @p_order_id bigint AS BEGIN SET NOCOUNT ON; SELECT o.order_number, o.ordered_on, o.total, a.name AS account FROM sales_order o JOIN account a ON a.id = o.account_id WHERE o.id = @p_order_id; SELECT p.sku, l.quantity, l.unit_price FROM order_line l JOIN product p ON p.id = l.product_id WHERE l.order_id = @p_order_id ORDER BY p.sku; END; GO CREATE PROCEDURE sp_account_report @p_account_id bigint AS BEGIN SET NOCOUNT ON; SELECT first_name, last_name, job_title FROM contact WHERE account_id = @p_account_id ORDER BY first_name; SELECT order_number, total FROM sales_order WHERE account_id = @p_account_id ORDER BY order_number; SELECT (SELECT COUNT_BIG(*) FROM contact WHERE account_id = @p_account_id) AS contacts, (SELECT COUNT_BIG(*) FROM sales_order WHERE account_id = @p_account_id) AS orders, (SELECT COALESCE(SUM(total), 0) FROM sales_order WHERE account_id = @p_account_id) AS revenue; END; GO -- Paging: a procedure that pages itself; a function ARX pages; a procedure's result set ARX pages. CREATE PROCEDURE sp_products_page @p_prefix varchar(32), @p_page int, @p_page_size int, @p_total bigint = NULL OUTPUT AS BEGIN SET NOCOUNT ON; SET @p_total = (SELECT COUNT_BIG(*) FROM product WHERE sku LIKE @p_prefix + '%'); SELECT sku, name, unit_price FROM product WHERE sku LIKE @p_prefix + '%' ORDER BY sku OFFSET (@p_page - 1) * @p_page_size ROWS FETCH NEXT @p_page_size ROWS ONLY; END; GO CREATE FUNCTION fn_products_like(@p_prefix varchar(32)) RETURNS TABLE AS RETURN SELECT sku, name, unit_price FROM product WHERE sku LIKE @p_prefix + '%'; GO CREATE PROCEDURE sp_products @p_prefix varchar(32) AS BEGIN SET NOCOUNT ON; SELECT sku, name, unit_price FROM product WHERE sku LIKE @p_prefix + '%' ORDER BY sku; END; GO -- Business rules the database enforces: an account on hold (its own error number) and a credit limit (THROW). CREATE PROCEDURE sp_reserve_credit @p_account_id bigint, @p_amount decimal(18, 2) AS BEGIN SET NOCOUNT ON; DECLARE @limit decimal(18, 2), @status varchar(10), @message nvarchar(400); SELECT @limit = credit_limit, @status = status FROM account WHERE id = @p_account_id; IF @status = 'ON_HOLD' BEGIN SET @message = CONCAT(N'account ', @p_account_id, N' is on hold: release the hold first'); THROW 50101, @message, 1; END; IF @p_amount > COALESCE(@limit, 0) BEGIN SET @message = CONCAT(N'credit limit exceeded: limit ', @limit, N', asked ', @p_amount, N'; reserve ', @limit, N' or less'); THROW 50102, @message, 1; END; UPDATE account SET credit_limit = credit_limit - @p_amount, modified_on = SYSDATETIMEOFFSET() WHERE id = @p_account_id; END; GO -- A nightly job that commits each batch and reports progress; a slow procedure. CREATE PROCEDURE sp_recalculate_totals @p_prefix varchar(20), @p_batch int AS BEGIN SET NOCOUNT ON; DECLARE @done int = 0, @rows int = 1; WHILE @rows > 0 BEGIN BEGIN TRANSACTION; UPDATE o SET total = COALESCE((SELECT SUM(l.quantity * l.unit_price) FROM order_line l WHERE l.order_id = o.id), 0) FROM sales_order o WHERE o.id IN (SELECT TOP (@p_batch) id FROM sales_order WHERE order_number LIKE @p_prefix + '%' AND total < 0 ORDER BY id); SET @rows = @@ROWCOUNT; COMMIT TRANSACTION; IF @rows > 0 BEGIN SET @done += @rows; RAISERROR(N'recalculated %d orders', 10, 1, @done) WITH NOWAIT; END; END; PRINT CONCAT(N'done: ', @done, N' orders'); END; GO CREATE PROCEDURE sp_import_progress @p_rows int AS BEGIN DECLARE @i int = 250; WHILE @i <= @p_rows BEGIN RAISERROR(N'imported %d of %d rows', 10, 1, @i, @p_rows) WITH NOWAIT; SET @i += 250; END; IF @p_rows > 900 RAISERROR(N'a large import: %d rows', 10, 1, @p_rows); END; GO CREATE PROCEDURE sp_slow @p_seconds int AS BEGIN DECLARE @delay datetime = DATEADD(second, @p_seconds, 0); WAITFOR DELAY @delay; END; GOConfigure the connection
The samples read the connection string from the
ARX_SAMPLES_SQLSERVERenvironment variable, never from the code. Set it in the terminal you run them from (with your password, where the database has one):Terminal macOS and Linux (bash, zsh)
export ARX_SAMPLES_SQLSERVER="Server=localhost,1433;Database=arx_samples;User Id=sa;Password=Your-password1;TrustServerCertificate=True"PowerShell Windows PowerShell
$env:ARX_SAMPLES_SQLSERVER = "Server=localhost,1433;Database=arx_samples;User Id=sa;Password=Your-password1;TrustServerCertificate=True"Add a sample and run it
Each sample page lists its files: copy each into the project with the file name shown, then run:
Terminal dotnet runThe page shows the output to expect. Ids may differ from yours if the tables already hold rows. Most samples write values that must be unique (an account number, a code), so a second run of the same sample fails with ARX3001: run the schema script again first, to start from empty sample tables.