Giter Club home page Giter Club logo

phorm's Introduction

Pho/rm - The Procedure-heavy object-relational mapping framework

Build & Test Code Coverage Mutation testing badge Code Quality

A full O/RM, focused on strong separation between the data structures and the business entity representation.

See our ethos for how and why Pho/rm is different to other O/RMs.

The wiki contains lots of useful examples of the various features, as well as a getting started guide.

Pho/rm supports:

Packages
IFY.Phorm.Core NuGet Version NuGet Downloads
IFY.Phorm.SqlClient NuGet Version NuGet Downloads

Driving principals

The are many, brilliant O/RM frameworks available using different paradigms for database interaction.
Many of these allow for rapid adoption by strongly-coupling to the storage schema at the expense of control over the efficiency of the query and future structural mutability.
As such solutions grow, it can become quickly difficult to evolve the underlying structures as well as to improve the way the data is accessed and managed.

Pho/rm was designed to provide a small and controlled surface between the business logic layer and the data layer by pushing the shaping of data to the data provider and encouraging the use of discrete contracts.
Our goal is to have a strongly-typed data surface and allow for a mutable physical data structure, where responsibility of the layers can be strictly segregated.

With this approach, the data management team can provide access contracts to meet the business logic requirements, which the implementing team can rely on without concern over the underlying structures and query efficiency.

flowchart RL
subgraph Database
    D[(Data)]
    V((vw))
    SP((sp))
end
subgraph Application
    O[DTO]
    I[/Interface/]
end

D -->|Get| O;
D --> V -->|Get| O;
SP -->|From.Get| O;
O -.->|Call/From| I --> SP --> D;
Loading

Common example

For typical entity CRUD support, a Pho/rm solution would require a minimum of:

  1. Existing tables in the data source
  2. A POCO to represent the entity (DTO); ideally with a contract for each database action
  3. A stored procedure to fetch the entity
  4. At least one stored procedure to handle create, update, delete (though, ideally, one for each)

A simple Pho/rm use would have the structure:

CREATE TABLE [dbo].[Data] (
    [Id] BIGINT NOT NULL PRIMARY KEY,
    [Key] NVARCHAR(50) NOT NULL UNIQUE,
    [Value] NVARCHAR(256) NULL
)

CREATE PROCEDURE [dbo].[usp_SaveData] (
    @Key NVARCHAR(50),
    @Value NVARCHAR(256),
    @Id BIGINT = NULL OUTPUT
) AS
    SET NOCOUNT ON
    INSERT INTO [dbo].[Data] ([Key], [Value])
        SELECT @Key, @Value
    SET @Id = SCOPE_IDENTITY()
RETURN 1 -- Success
// DTO and contracts
[PhormContract(Name = "Data")] // Name of underlying table (optional)
class DataItem : ISaveData
{
    public long Id { get; set; }
    public string Key { get; set; } = string.Empty;
    public string? Value { get; set; }
}
interface ISaveData : IPhormContract
{
    long Id { set; } // Output
    string Key { get; }
    string? Value { get; }
}

// Configure Pho/rm session to SQL Server
IPhormSession session = new SqlPhormSession(connectionString);

// Get all existing records from the table
DataItem[] allData = session.Get<DataItem[]>()!; // Table dbo.Data

// Add a new record to the table, getting back the new id
var newItem = new { Id = ContractMember.Out<long>(), Key = "Name", Value = "T Ester" };
int result = session.Call<ISaveData>(newItem); // Procedure dbo.usp_SaveData

DataItem? itemById = session.Get<DataItem>(new { Id = newItem.Id }); // Table dbo.Data
DataItem? itemByKey = session.Get<DataItem>(new { Key = "Name" }); // Table dbo.Data

Syntax overview

IPhormSession
    // Calling a contract
    int Call(string contractName, object? args = null);
    int Call<TActionContract>(object? args = null);
    Task<int> CallAsync(string contractName, object? args = null, CancellationToken cancellationToken = CancellationToken.None);
    Task<int> CallAsync<TActionContract>(object? args = null, CancellationToken cancellationToken = CancellationToken.None);

    // Fetching from a DTO definition (table, view)
    TResult? Get<TResult>(object? args = null);
    Task<TResult?> GetAsync<TResult>(object? args = null, CancellationToken cancellationToken = CancellationToken.None);

    // Fetching from a named procedure
    From(string contractName, object? args = null)
        TResult? Get<TResult>();
        Task<TResult?> GetAsync<TResult>(CancellationToken cancellationToken = CancellationToken.None);
        // Resultset filtering
        Where<TEntity>(Expression<Func<TEntity, bool>> predicate)
            IEnumerable<TEntity> GetAll();
            Task<IEnumerable<TEntity>> GetAllAsync(CancellationToken cancellationToken = CancellationToken.None);
    
    // Fetching from a contract definition (procedure, table, view)
    From<TActionContract>(object? args = null)
        TResult? Get<TResult>();
        Task<TResult?> GetAsync<TResult>(CancellationToken cancellationToken = CancellationToken.None);
        // Resultset filtering
        Where<TEntity>(Expression<Func<TEntity, bool>> predicate)
            IEnumerable<TEntity> GetAll();
            Task<IEnumerable<TEntity>> GetAllAsync(CancellationToken cancellationToken = CancellationToken.None);

phorm's People

Contributors

ifyates avatar

Stargazers

 avatar  avatar

Watchers

 avatar

phorm's Issues

Enum transformation does not support valid non-`int` values from datasource

Reproducible Example

-- Source
SELECT CONVERT(TINYINT, 1) [Enum]
// DTO
class DTO {
    public MyEnum Enum { get; set; }
}

phorm.Get<DTO>(); // InvalidCastException: Invalid cast from 'System.Byte' to 'MyEnum'.

Expected behavior
Get of a numeric value is converted to the enum equivalent.

Environment (please complete the following information):

  • Version 1.7.0

Test tools

Depending on how "dirty" mocking Pho/rm use is in real projects, a new package providing a set of tools to make mocking easier.

Idea: a MockPhormSession and/or MockPhormContractRunner structure that consumes a clearer interface for mocking.

Example:

var session = new MockPhormSession(myPhormTestMock.Object);

var x = session.From<IAction>(args).Get<Data[]>()!; // Invokes: myPhormTestMock.GetFrom<IAction, Data[]>(args)

Ideally, also able to optionally verify DbObjectType of call and args structure (esp. when anonymous object).

Support DateOnly

Test current state of DateOnly support. Improve as necessary.

Remaining items before release

1.3.0-beta

1.4.0-beta

  • Connection context naming
  • Connection pool testing

1.5.0 (Release)

  • README review -> wiki
  • Type caching
  • Increase test coverage

Non-functional

  • GitHub Actions (CI/CD)
  • Stryker

Improve syntax

Currently, getting from a table looks like this:
phorm.From<Entity>().Many<Entity>(args)

Clearer would be:
phorm.FromTable().Many<Entity>(args)
or
phorm.From<Entity>().Many(args)

Non-core encryption classes to new package

Everything not required by Core can be moved to a new IFY.Phorm.Encryption.Core package.
This should include SecureValueAttribute, IEncryptorProvider and IEncryptor, etc.

These are not required unless using encryption.

The AbstractSecureValueAttribute inheritance is the only part used to plumb in to the transformation system.

Separate .NET package

Some of the .NET Standard -> .NET changes may provide minor performance improvements (notable around pattern matching).

If this can be demonstrated, a separate .NET package should be released, with the .NET Standard package being maintenance only.

Idea would be v1.x for .NET Standard and v2.x for .NET, with minor versions aligned around functional changes.

GenSpec support

Natural support for GenSpec entities.

e.g., call Many<BaseUser> where instances are actually specific (StandardUser, AdminUser) with specific properties.

How to receive data:
Opt 1: resultset contains superset of possible properties and only uses ones per type (a lot of unions)
Opt 2: resultset per type, each with correct properties

Need way of specifying types to map:
Opt 1: resultset contains identifier (loose mapping, easily broken)
Opt 2: attribute on base class for different specifics (requires ownership of base, still need a way to map)
Opt 3: send types to call (more verbose/fixed, but clear)

POC
phorm.From<BaseUser>().GenSpec<TypeA, TypeB, ...>().Many(args)

Interface implemented properties not resolving on call contract

Describe the bug
Interface implemented properties not resolving on call contract.

Reproducible Example

interface IEntity
{
    string Value => "value";
}

_ = session.Call<IEntity>();
// SQL: EXEC [schema].[usp_Entity]

Expected behavior

// SQL: EXEC [schema].[usp_Entity] @Value = 'value'

Implement an out-of-the-box RSA encryption package

Standard symmetric/asymmetric model using certificates.

A new IFY.Phorm.Encryption.RSA package that provides the IEncryptor, IEncryptorProvider implementations for a fully-versionable implementation (e.g., with data-versioning).

Potentially should first have a IFY.Phorm.Encryption.AES package that provides the symmetric encryption implementation. Fully usable.
For management purposes, probably best left until later.

Dynamic removal of members

A way to attribute members and remove them from the contract dynamically.

One idea would be a way for a Transphorm to return something indicating that the member is to be ignored.

e.g., if FromDatasource or ToDatasource returns an instance of IgnoreDataMemberAttribute (or the type), don't change the value, but ignore the member.

object? FromDatasource(Type type, object? data, object? context) => new IgnoreDataMemberAttribute();
object? FromDatasource(Type type, object? data, object? context) => typeof(IgnoreDataMemberAttribute);

Support runtime additional resultsets

Assuming it is sometimes required to return different additional resultsets, support this without using hard-coded resultset indexes.

e.g., currently, sproc1 returns three resultsets: primary, "0" and "1", so that primary entities can have 2 types of child attached.
If we wanted the same models to sometimes only return some types of child, we'd need to have an empty resultset for "0".

Alternative would be either dynamic detection of resultsets:

  1. Required column __ChildType__ that specifies a bindable value
    • Pro: small change
    • Con: requires change to resultset (harder if reusing other logic, though not destructive)
  2. The first resultset after the primary is a list of types of the followings resultsets
    • Pro: separate change
    • Con: slightly heavy

Or code-side specification of children: e.g., Get<Type1>().And<Type2>(t1 => t1.Child1Prop).And<Type3>(t1 => t1.Child2Prop) where each And takes the mapping info.

  • Pro: very easy to extend
  • Con: too much wiring at use?

Could we select the child properties instead? e.g., the t1 => t1.Child1Prop is what defines the next child mapping

How to handle deeper levels?

Transformed values may happen before literals

Describe the bug
If an object contains a transformed property that relies on other properties, it is impossible to determine if the values are processed in the correct order.

Reproducible Example

class DTO
{
    public string Prop1 { get; set; }
    [TransformLogic] // Changes Prop2 values based on Prop1 and Prop3
    public string Prop2 { get; set; }
    public string Prop3 { get; set; }
}

At the point Prop2 transformation is resolved, it is indeterminate if other properties have been resolved.

Expected behavior
TransformLogic is applied to Prop2 correctly, making use of Prop1 and Prop3 values.

Environment (please complete the following information):

  • Version: 1.6.0
  • Platform: All

Override contract member direction by attribute

Currently, to send a non-OUTPUT member, you need this setup:

// Full
interface IMyContract : IPhormContract { string Arg { get; } }
class MyContract : IMyContract { public string Arg { get; set; } }
session.From<IMyContract>(new MyContract { Arg = "value" });

// Ad-hoc
interface IMyContract : IPhormContract { string Arg { get; } }
session.From<IMyContract>(new { Arg = "value" });

Using MyContract directly will fail because Arg will be treated as InputOutput (due to having get and set).

Proposed new attribute:

class ContractMemberAttribute : DataMemberAttribute // Can also provide Name, etc.
{
    public bool IgnoreAsInput { get; set; }
    public bool IgnoreAsOutput { get; set; }
}

Minimal contract use is now:

class MyContract : IPhormContract
{
    [ContractMember(IgnoreAsOutput = true)]
    public string Arg { get; set; }
}
session.From(new MyContract { Arg = "value" });

Contract out parameter fails if anon object doesn't define it

Describe the bug
If an anonymous object is missing an out property defined in the contract, an exception is thrown.

Reproducible Example

interface IContract
{
    int Arg { set; }
}

var arg = new { };
session.Call<IContract>(arg);  // Exception

Expected behavior
The execution completes and the out value is ignored.

Lazy entity resolution of collections

In a situation where a resultset is potentially massive and much of it may be discarded, it would be ideal to not require the returned list to contain every entity.

Two different (and both valuable) approaches:

  1. Return unresolved IEnumerable<TEntity> where iteration causes the entities to be resolved
    • Note: resolution must only occur once (i.e., subsequent calls to ToArray() are light)
  2. A lower-level hook for filtering recordsets before they are resolved to entities
    a. Handling of expression tree to resolve entity properties only as needed
    b. Ability to check the raw record (readonly)
    c. Syntax for resolving subset of entity for predicate

Allow decryptor to be chosen using encrypted data

Currently, the selection of the IEncryptor to use for encryption and decryption is based solely on the dataClassification value.
The actual data to be decrypted is only checkable at the point of decryption.

There are decryption scenarios where the choice of implementation is only going to be resolvable once the data is known, through versioning, etc.

With the current structure, the IEncryptorProvider would need to return an IEncryptor that then invokes the implementation based on the data.

If the IEncryptorProvider also took in the data when selecting the decryptor, the IEncryptor chosen could be for the appropriate implementation, correctly separating the "provider" from the logic.

Support record types with default constructors

Given entity:

[PhormContract] public record MyDTO(long Id, string Value);

Make it possible to retrieve a result using:

var results = phorm.Get<MyDTO[]>();

Currently fails because MyDTO only has the default record constructory.
Equivalent to public MyDTO(long Id, string Value) { ... }.

Transactions not working

Attempting to use a transacted session causes exceptions.

The first is around connection being already open (internal call to db.Open() when state is already Open).

The second is that the internal command used does not have the current transaction attached.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.