Skip to content

HowTo Call

Ian Yates edited this page Nov 14, 2022 · 9 revisions

Contents


Action contracts (no expected resultset) are invoked with the Call and CallAsync methods.

All examples below use the CallAsync syntax but are equally valid for Call.

Contracts

There are two ways to invoke an action contract:

  1. Ad hoc
  2. Interface

Ad hoc

This is not the preferred Pho/rm way, but it's possible to invoke an action contract by name without creating any additional code objects.

int result = await session.CallAsync("MyContract");
// SQL: EXEC [dbo].[usp_MyContract]

Interface

The preferred way to invoke an action contract is through a well-defined contract interface:

interface IMyContract : IPhormContract
{
    // No parameters
}

The result is essentially the same:

int result = await session.CallAsync<IMyContract>();
// SQL: EXEC [dbo].[usp_MyContract]

Parameters

There are two ways to pass parameters to action and data contracts:

  1. Anonymous
  2. Entity

The below assumes an action contract like:

CREATE PROCEDURE [dbo].[MyContract] (
    @Arg1 INT,
    @Arg2 VARCHAR(50) = NULL -- Optional
) AS
    SET NOCOUNT ON

    -- TODO: logic

RETURN 1

Using contract interface:

interface IMyContract : IPhormContract
{
    int Arg1 { get; }
    string? Arg2 { get; }
}

Anonymous

Any object that completes the interface, including an anonymous one:

int result = await session.CallAsync<IMyContract>(new { Arg1 = 99 });
// SQL: EXEC [dbo].[usp_MyContract] @Arg1 = 99

Entity

A concrete implementation of the interface:

class MyContractImpl : IMyContract
{
    public int Arg1 { get; set; }
    public string? Arg2 { get; set; }
}

int result = await session.CallAsync<IMyContract>(new MyContractImpl { Arg1 = 99 });
// SQL: EXEC [dbo].[usp_MyContract] @Arg1 = 99

This could be the main entity DTO or a class specifically for use as a contract argument.

Addendum

For data contracts, this is the same style but using the From<?>(...).Get format.

The main factor for deciding which to use (apart from personal preference) will likely be any output or result information.

Note that omitting a required parameter or sending the wrong datatype will cause an exception to be thrown.

Combining both of the above sections, a truly ad hoc action contract call could look like:

int result = await session.CallAsync("MyContract", new { Arg1 = 99 });
// SQL: EXEC [dbo].[usp_MyContract] @Arg1 = 99

Output

Action contracts can receive "output" data from the datasource.

CREATE PROCEDURE [dbo].[usp_Action]
    @Value INT OUTPUT = NULL
AS
    SET NOCOUNT ON
    SET @Value = 999
RETURN 1
interface IMyAction : IPhormContract
{
    int Value { set; } // "set" means we expect an output value
}
class ActionEntity : IMyAction
{
    public int Value { get; set; }
}

var arg = new ActionEntity();

int result = await session.CallAsync<IMyAction>(arg);
// SQL: EXEC [dbo].[usp_Action] @Value OUTPUT
// arg.Value will now be 99

Contract members with both get and set will send the existing value and can be updated by the procedure:

interface IMyAction : IPhormContract
{
    int Value { get; set; }
}

var arg = new ActionEntity { Value = 50 };

int result = await session.CallAsync<IMyAction>(arg);
// SQL: EXEC [dbo].[usp_Action] @Value OUTPUT = 50
// arg.Value.Value may no longer be 50

Alternatively, an anonymous object can achieve the same using a ContractMember instance:

var arg = new
{
    Value = ContractMember.Out<int>() // Use InOut to also send a value on CallAsync
};

int result = await session.CallAsync<IMyAction>(arg);
// SQL: EXEC [dbo].[usp_Action] @Value OUTPUT
// arg.Value.Value will now be 99

Member attributes

Outgoing contract members can be skipped using IgnoreDataMemberAttribute:

interface IMyContract : IPhormContract
{
    int Id { get; }
    [IgnoreDataMember] string Value { get; }
}

int result = await session.CallAsync<IMyContract>(new { Id = 123, Value = "test" });
// SQL: EXEC [dbo].[usp_MyContract] @Id = 123

Members can be renamed using the Name property of DataMemberAttribute:

interface IMyContract : IPhormContract
{
    [DataMember(Name = "NewId")]
    int Id { get; }
}

int result = await session.CallAsync<IMyContract>(new { Id = 123 });
// SQL: EXEC [dbo].[usp_MyContract] @NewId = 123

The EmitDefaultValue property (default false) can be set to force default values to be sent on the contract:

interface IMyContract : IPhormContract
{
    int Id { get; }
    [DataMember(EmitDefaultValue = false)] string? Value1 { get; }
    [DataMember(EmitDefaultValue = true)] string? Value2 { get; }
}

int result = await session.CallAsync<IMyContract>(new { Id = 123 });
// SQL: EXEC [dbo].[usp_MyContract] @Id = 123, @Value2 = NULL

Finally, the IsRequired property (default false) can be set to cause an exception if the contract is sent incomplete:

interface IMyContract : IPhormContract
{
    int Id { get; }
    [DataMember(IsRequired = true)] string? Value { get; }
}

int result = await session.CallAsync<IMyContract>(new { Id = 123 });
// MissingMemberException is thrown

Calculated members

Note: Due to the limitations described here, implemented interface members are not recommended and support may be removed in future versions of Pho/rm.

Implemented interface properties will be sent like other contract members, if applied to an implementing class:

interface IMyContract : IPhormContract
{
    int Id { get; }
    public string Value => "test";
}
class MyEntity : IMyContract
{
    public int Id => 123;
}

int result = await session.CallAsync<IMyContract>(new MyEntity());
// SQL: EXEC [dbo].[usp_MyContract] @Id = 123, @Value = 'test'

Unfortunately, because anonymous objects do not implement the interface, the implementation is ignored and the property must be supplied like a normal property:

int result = await session.CallAsync<IMyContract>(new { Id = 123, Value = "new test" });
// SQL: EXEC [dbo].[usp_MyContract] @Id = 123, @Value = 'new test'

For more complex logic, method results can be included to provide additional values using ContractMemberAttribute, with the same limitation:

interface IMyContract : IPhormContract
{
    int Id { get; }

    [ContractMember]
    public string Value()
    {
        return "test2";
    }

    public string OtherValue() => "test3"; // Methods are ignored by default
}
class MyEntity : IMyContract
{
    public int Id => 123;
}

int result = await session.CallAsync<IMyContract>(new MyEntity());
// SQL: EXEC [dbo].[usp_MyContract] @Id = 123, @Value = 'test2'

When using anonymous objects, the value can be supplied like a normal property:

int result = await session.CallAsync<IMyContract>(new { Id = 123, Value = "new test" });
// SQL: EXEC [dbo].[usp_MyContract] @Id = 123, @Value = 'new test'

Datatype mappings

Pho/rm will attempt to send the following types based on property type:

C# Type MS SQL Type
byte[x] BINARY(x)
DateOnly DATE
DataTime DATETIME2
Guid UNIQUEIDENTIFIER
enum* INT / VARCHAR(x)

All other types automatically handled by provider.

Note that most RDBMS implementations will gracefully handle type differences where possible.