Repodb: Feature: Introduce the ClassHandler Support

Created on 25 Aug 2020  路  12Comments  路  Source: mikependon/RepoDB

In order for RepoDB to be more hybrid as usual, we need to introduce the ClassHandler so the users will be able to hook-up an event before and after the class serialization and deserialization process.

How does ClassHandler works?

If you are reading a data from the DB (i.e. ExecuteQuery, Query, BatchQuery), there should be a method that you can hook-up to make a post change on the newly deserialized model. On the other hand, if you are pushing a data into the DB (i.e. Insert, Merge, Update), there should be a method that you can hook up to validate or make a pre-change prior the actual DB operation.

Desired Implementation

An interface IClassHandler<TEntity> or IClassHandler must be introduced. This interface must have the following methods:

  • Get - this method must be invoked after the read operations.
  • Set - this method must be invoked before the push operations.

You should implement a class handler class that implements this interface like below.

public class PersonClassHandler : IClassHandler<Person>
{
    public void Get(Person person, DbDataReader reader)
    {
        // Code after the read operations
    }

    public void Set(Person person)
    {
        // Code before the push operations
    }
}

Note: The TEntity within the IClassHandler interface is up for discussion, this can be eliminated.

The Get method is of return type void. That is on purpose as the instance passed is referenced to the original object. The instance of DbDataReader in used will also be passed on the method, able the user to counter validate the contents against the actual DbDataReader.

Attribute

Below would be the attribute.

public class ClassHandler : Attribute
{
    public ClassHandler(Type classHandlerType)
    {
        ...
    }
}

Mapping

Below would be the process to enable the handler on your model.

Via FluentMapper

FluentMapper.Entity<Person>().ClassHandler<PersonClassHandler>();

Via ClassHandlerMapper

ClassHandlerMapper.Add<Person, PersonClassHandler>();

Via Attribute

[ClassHandler(typeof(PersonClassHandler<Person>))]
public class Person
{
    ...
}

Or even without the <TEntity>.

[ClassHandler(typeof(PersonClassHandler))]
public class Person
{
    ...
}

This feature is up and is open for discussion with the .NET community. Please feel free to share yours.

References

  • Help: How to dynamically add non db value to object property. #518 - @Kodestuen
Fixed deployed feature priority request todo

All 12 comments

I have two questions mainly

  1. How would the class handler work for immutable classes? since they can't actually update their values
    I think this could be solved by an overload for the get/set methods that return what their value is an example from F#
type IClassHandler<'T> =
    abstract Get: model:'T * reader:obj -> 'T
    abstract Set: model:'T -> 'T

type Person =
    { name: string
      longstringpropery: string }

    /// both methods return the updated copy of the model
    interface IClassHandler<Person> with
        member _.Get(model, reader) =
            { model with
                  name = model.name.ToUpper() }

        member _.Set(model) =
            { model with
                  longstringpropery = model.longstringpropery.Substring(120) }

The second question (which is kind of shown in the example)

  1. Can the same model/record implement this interface or would it be better to abstract away this logic in a separate class?

in the case, it's the second I think it would be cautious to have object expressions in mind so there aren't compatibility issues I'm thinking something like this

let personHandler() = 
   { new IClassHandler<Person> with 
         member _.Get(model, reader) =
              { model with
                    name = model.name.ToUpper() }

         member _.Set(model) =
              { model with
                    longstringpropery = model.longstringpropery.Substring(120) }

ClassHandlerMapper.Add<Person>(personHandler());

@AngelMunoz - For your question #1. I initially thought that the Get method should return the TEntity. In the case of immutable class, within the Get method, a new instance of that immutable class can be created and returned. Do you think it would be your case?

That is on the case like: Let us say, you fail the validation in the Get method and you are force to return the new entity, like below.

Let us assume the Person model is immutable.

public class PersonClassHandler : IClassHandler<Person>
{
    public Person Get(Person person, DbDataReader reader) // <-- this returns the TEntity itself
    {
        if (ValidatePerson(person) == false) // Assume you have the ValidatePerson() method
        {
            return new Person(person.Id, person.Name, ...);
        }
        return person;
    }

    public void Set(Person person)
    {
        // Code before the push operations
    }
}

For your question #2. TBH, it is not difficult to support if the 'self' model/record would implement the interface itself. But, for the uniformity and cleanliness purposes, also for code organizations, I would highly recommend to have it implemented as a separate class handler class. Imagine the following Union architecture and code organizations.

SLN
+ ClassHandlers
   - PersonClassHandler.cs
   - CustomerClassHandler.cs
   - OrderClassHandler.cs
+ DTOs
+ Interfaces
+ Models // <-- I find it dirty to combine here
   - Person.cs
   - Customer.cs
   - Order.cs
+ PropertyHandlers
--> Repositories

Do you think it would be your case?

I used to work with a nodejs orm which had some hooks before and after insertion basically like these

  • before validate
  • after validate
  • before insert
  • after insert

if any of those failed the rest of the chain wouldn't be executed I don't think before/after validate are entirely necessary I guess you could get away with just a Validate Stage which would implement an interface with a bool record

type IValidateFor<'T>
  abstract member Validate: model: 'T -> bool
  //optional - I don't really have a use case for these but for completeness I mention them
  abstract member BeforeValidate: model: 'T -> bool
  abstract member AfterValidate: model: 'T -> bool

I that case if you validated prior and failed to validate you wouldn't execute the Get and deal with validation failures in there and when the Get is executed you could be sure that the model is valid

I would highly recommend to have it implemented as a separate class handler class.

got it, I was kind of leaning towards that but I just wanted to make sure

I also think of the different layers of class handling before. I had it explained with @Kodestuen as well, class handler were considered before but we never introduced it. But, is that really a case here, or would that fall on 1% of the cases (or an edge cases).

As of writing this, I am thinking that it could complicate the things, both the implementation and the support and to be honest, maybe the actual usability?

Also, please take note that the more layer we have the more slower the deserialization/serialization process. But, multi layer class handling is really a good thing to consider. Do you think you need that or can we make a poll from every body?

Do you think you need that or can we make a poll from every body?

I believe it is nice to have. I have used it before and it's nice to have those things in place but I don't feel is required. It wouldn't hurt to have a poll though

These Events? (BeforeGet, AfterGet, BeforeSet, AfterSet) - EDIT: Again I prefer simplicity and directness for easy usability, but please help define your case. The purpose of ClassHandler is not just for validation, it is also use to any (or even unlimited) use cases like PropertyHandler.

Sure,

these are the following cases I have had before in a similar fashion to #518

  • I have a payment model that once I have inserted into the database (AfterSet) I update other models and areas in the application in response that a payment has been made, in this stage, I can also trigger emails and notifications
  • Before saving (BeforeSet) the User I update the password with a properly hashed password, also I check other properties to ensure they fit the proper format and business rules (validation)

While those are the use cases I agree on your take on simplicity I have had used without these "hooks" before.

Cool. Sounds like a great use-case - that's being said and it is acknowledge. It will be a part of the ClassHandler release.

@AngelMunoz - I just realized that the before event of the Get method (i.e. Fetch operations like Query, BatchQuery and ExecuteQuery) is not really feasible for the Immutable classes.

Think of this scenario: A) You query a record, B) then the engine will first create an empty model instance, C) trigger the Get method by passing the newly created empty model, D) then set the model properties based from the values of the DbDataReader. For the immutable classes, such scenario is impossible and that would invalidate this event method.

Note: I will try to think over tonight how can I address this, otherwise, I would implement the simpliest form of handler with only Get and Set method.

I hope it is okay with you and you find it fair enough.

I see, like I mentioned before it would be a nice to have but not something I'd consider required, those are blocking issues that do seem reasonable it's okay with the simplest form

The fix is now available at RepoDB v1.12.0-beta1.

Closing this ticket now. Thanks!

Was this page helpful?
0 / 5 - 0 ratings