-
Notifications
You must be signed in to change notification settings - Fork 0
HowTo Transphormation
Contents
Pho/rm supports extension decoration for transforming data to/from the datasource.
By default, JSON and enum "transphormations" are included.
The JsonValueAttribute
current implementation uses Newtonsoft JSON.NET for JSON serialisation and deserialisation, due to the low requirement for additional object scaffolding.
This will be reviewed regularly until System.Text.Json is the preferred implementation, or the concensus is to remove native JSON support.
Sending as an action parameter:
record MyData(string Value1, int Value2);
interface IMyActionContract : IPhormContract
{
[JsonValue] MyData Data { get; }
}
var arg = new
{
Data = new MyData("some text", 12345)
};
int result = session.Call<IMyActionContract>(arg);
// SQL: EXEC [dbo].[usp_MyActionContract] @Data = '{ "Value1": "some text", "Value2": 12345 }'
Receiving from a result:
CREATE TABLE [dbo].[Entity] (
[Data] NVARCHAR(MAX) NOT NULL -- Suggest NVARCHAR(MAX) for JSON data
)
INSERT INTO [dbo].[Entity] ([Data])
SELECT '{ "Value1": "some text", "Value2": 12345 }'
[PhormContract(Target = DbObjectType.Table)]
record Entity([property: JsonValue] MyData Data);
var result = session.Get<Entity>();
// SQL: SELECT [Data] FROM [dbo].[Entity]
// result.Data.Value1 == "some text"
// result.Data.Value2 == 12345
The JSON serialiser settings can be changed by setting the IFY.Phorm.GlobalSettings.NewtonsoftJsonSerializerSettings
property.
Pho/rm will implicitly deal with enum value transformation without any help, but the EnumValueAttribute
can be used to override default behaviour.
enum MyState
{
State1 = 1,
State2,
[EnumMember("State3")]
StateX
}
// Get will implicitly support receiving a numeric value (1, 2, 3) or string of "State1", "State2", or "State3" (but not "StateX")
record Record (long Id, MyState State);
// Call using this contract will send the string equivalent ("State1", "State2", "State3")
interface IRecord_UpdateState : IPhormContract
{
long Id { get; }
[EnumValue(SendAsString = true)] MyState State { get; }
}
// No attribute, will always send the numeric value
Record[] results = session.Get<Record[]>(new { State = MyState.State2 })!;
// SQL: SELECT [Id], [Sate] FROM [dbo].[Record] WHERE [State] = 2
// With attribute, can be told to send the string (using EnumMember overrides)
int result = session.Call<IRecord_UpdateState>(new { Id = 1, State = MyState.StateX });
// SQL: EXEC [dbo].[usp_Record_UpdateState] @Id = 1, @State = 'State3'
To define your own input/output transformers, you must define a new "transphorm" attribute.
In this example, we'll simply force text sent to storage to be in uppercase but always used as lowercase:
public class CaseTransformerAttribute : AbstractTransphormAttribute
{
// Uppercase in storage, but we want lowercase in the business layer
public override object? FromDatasource(Type type, object? data, object? context)
{
if (type != typeof(string))
{
return null; // Invalid value for this transformation
}
return data?.ToString().ToLower();
}
// Lowercase in the business layer, but we want to store uppercase
public override object? ToDatasource(object? data, object? context)
{
return data?.ToString().ToUpper();
}
}
This can then be used on any contract member:
class MyEntity
{
[CaseTransformer] public string Text { get; set; } // Lowercase from storage
}
interface IMyEntityContract : IPhormContract
{
[CaseTransformer] string Text { get; } // Uppercase to storage
}
Only one transformation is possible per property, and there is no enforcement that DTOs and contracts apply the same transformations to properties.
If your transformation logic needs to ignore the property as though it were not a member of the contract, you can return the IgnoreDataMemberAttribute
(type or instance).
public class IgnoreNegativeAttribute : AbstractTransphormAttribute
{
public override object? FromDatasource(Type type, object? data, object? context)
=> data as int i && i >= 0 ? i : typeof(IgnoreDataMemberAttribute);
public override object? ToDatasource(object? data, object? context)
=> data as int i && i >= 0 ? i : new IgnoreDataMemberAttribute();
}
class MyEntity
{
[IgnoreNegative] public int Number { get; set; }
}
interface IMyEntityContract : IPhormContract
{
[IgnoreNegative] int Number { get; }
}
MyEntity data = session.From<IMyEntityContract>(new { Number = -1 })
.Get<MyEntity>();
// SQL: EXEC [schema].[usp_MyEntityContract] -- NOTE: no @Number parameter
// A negative 'Number' value in the resultset will be treated the same as the column not being returned (different to receiving a '0' value)
Pho/rm supports application-level encryption of data in either direction to the datasource using SecureValueAttribute
:
record RecordDTO (
long Id,
[property: SecureValue("DataClassification", nameof(Id))] string Data // Decrypt on receipt
);
interface IRecord_UpdateData
{
long Id { get; }
[SecureValue("DataClassification", nameof(Id))] string Data { get; } // Encrypt on send
}
These properties will be processed using the relevant IEncryptor
instance, based on the registered provider's handling of the DataClassification
value:
class MyEncryptionProvider : IEncryptionProvider
{
public IEncryptor GetDecryptor(string dataClassification, byte[] data) => new MyEncryptor();
public IEncryptor GetEncryptor(string dataClassification) => new MyEncryptor();
}
class MyEncryptor : IEncryptor
{
public byte[] Authenticator { get; set; } // Provided by framework, as needed
public byte[] InitialVector { get; }
public byte[] Decrypt(byte[] data)
{
// TODO: decrypt data using this state
}
public byte[] Encrypt(byte[] value)
{
// TODO: encrypt value using this state
}
}
// Registered at startup
IFY.Phorm.GlobalSettings.EncryptionProvider = new MyEncryptionProvider();
Currently, the encryption framework supports properties with type byte
, byte[]
, char
, DateOnly
, DateTime
, decimal
, double
, float
, Guid
, int
, long
, short
, or string
. Any other property type will throw an InvalidCastException
.
Encryption and transformations can be combined and will work as expected:
class MyEntity
{
long Id { get; }
// Decrypts from storage and then converts to lowercase
[SecureValue("DataClassification", nameof(Id)), CaseTransformer]
public string Text { get; set; }
}
interface IMyEntityContract : IPhormContract
{
long Id { get; }
// Converts to uppercase and then encrypts to storage
[SecureValue("DataClassification", nameof(Id)), CaseTransformer]
string Text { get; }
}