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.
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.
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.
Below would be the attribute.
public class ClassHandler : Attribute
{
public ClassHandler(Type classHandlerType)
{
...
}
}
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
I have two questions mainly
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)
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
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
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!