Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Interception for object materialization (a.k.a. "ObjectMaterialized") #15911

Closed
Tracked by #24106
ajcvickers opened this issue Jun 3, 2019 · 27 comments · Fixed by #28274
Closed
Tracked by #24106

Interception for object materialization (a.k.a. "ObjectMaterialized") #15911

ajcvickers opened this issue Jun 3, 2019 · 27 comments · Fixed by #28274
Labels
area-dbcontext area-query closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. ef6-parity type-enhancement
Milestone

Comments

@ajcvickers
Copy link
Contributor

Splitting this out as a separate issue as required on #626 so we can track it independently of other life-cycle hooks.

The ObjectMaterialized event will fire after an entity instance has been created and all it's non-navigation properties have been set.

@SidShetye
Copy link

@ajcvickers thanks for breaking this down into even smaller work items. Wasn't sure what granularity @divega was looking for.

Anyway, could this be promoted to 3.0.0 milestone just like #15910 ? The reason being we need hooks at both paths to and from the database - to preserve symmetry and avoid data corruption during data transformations. Having both ObjectMaterialized and SavingChanges is the bare minimum for us to unblock our EF Core customers who've been patiently asking us to support EF Core for a very long time. At our end, we can spotlight the 3.0.0 release as "fully supported" when it eventually comes out.

Thanks

@ajcvickers
Copy link
Contributor Author

@SidShetye Can you give some details on how you intend to use ObjectMaterialized?

@SidShetye
Copy link

@ajcvickers : Sure. We basically use integration points in any ORM to enforce our data security pipeline.

Specifically, for data about to be written to the database we'd use something like SavingChanges in EF that results in encrypted data being sent to the database.

So on the reverse path, we again need our security pipeline to kick in that results in plaintext data being presented to the EF application. ObjectMaterialized in EF 6.x allows us that attach point and we'd like to use it similarly in EF Core too. As you can imagine, without anything like ObjectMaterialized on the reverse path, the data remains in an unusable state. For a visual, check out the very first figure on page 1.

@SidShetye
Copy link

SidShetye commented Jun 28, 2019

@ajcvickers - will this be marked for the 3.0 release? Did my response address your question on usage? Thanks

@ajcvickers
Copy link
Contributor Author

@SidShetye It's pretty unlikely to go into 3.0 at this point.

@SidShetye
Copy link

@ajcvickers Could we please have this for 3.1?

@optiks
Copy link

optiks commented Jul 24, 2019

@SidShetye Have you considered using value conversions instead? Presumably you have a mechanism for flagging which properties need to be encrypted?

You could also consider introducing an Encrypted (potentially with an implicit operator) and expose that in your domain.

https://docs.microsoft.com/en-us/ef/core/modeling/value-conversions

@ajcvickers
Copy link
Contributor Author

@SidShetye We haven't decided what will be in 3.1 yet.

@nphmuller
Copy link

nphmuller commented Sep 20, 2019

With this event we could implement child entity ordering in a way that could simplify our code-base a lot.

We used to have a (pretty unoptimized) implementation that relied on internal EFCore classes. This solution broke when transitioning to EFCore 3.0 (as expected, but it was a risk we where willing to take at the time). For now we will probably order manually per entity type that needs it. But it would be great if we configure this globally for the entity type, which this event would allow.

@EMooreMAC
Copy link

We need this feature because our entities track their own changes independently of the DbContext, and our framework needs to know the difference between a property being set on initialization vs later. In EF6, we can set an IsInitializing flag to false after the object is materialized, and our tracking code (in property setters) can ignore property changes that happen during initialization.

I'm open to ideas as to how else we could achieve similar functionality.

@optiks
Copy link

optiks commented Jan 30, 2020

I'm open to ideas as to how else we could achieve similar functionality.

@EMooreMAC, I’ve solved this in the past. Give me a few days and I’ll try and get some code for you.

@StevenRasmussen
Copy link

Ok.... posting this for anyone else that would like a workaround in the short term until this is implemented. This isn't fully tested so use at your own risk... and with the caveat that the underlying code may change at any time since it is relying on some EF Core internals. Perhaps someone from the EF team could comment on how stable/usable this might be for the short term. That being said... I'm using the code with great success.

Declare an interface for methods to be invoked

interface IMaterialize
{
    void OnMaterializing();
    void OnMaterialized();
}

Implement the interface on your model:

class YourModel : IMaterialize
{
    public void OnMaterializing()
    {
        // Called after the constructor has been called and object created
    }
    public void OnMaterialized()
    {
        // Called after all properties have been populated
    }
}

Create a new implementation of the EntityMaterializerSource class which inherits from the EFCore implementation but overrides one of the methods:

class MyEntityMaterializerSource : Microsoft.EntityFrameworkCore.Query.EntityMaterializerSource
{
    public MyEntityMaterializerSource(EntityMaterializerSourceDependencies dependencies)
        : base(dependencies)
    {
    }

    public override Expression CreateMaterializeExpression(IEntityType entityType, string entityInstanceName, Expression materializationContextExpression)
    {
        var baseExpression = base.CreateMaterializeExpression(entityType, entityInstanceName, materializationContextExpression);
        if (entityType.ClrType.GetInterfaces().FirstOrDefault(i => i == typeof(IMaterialize)) != null)
        {
            var onMaterializingMethod = entityType.ClrType.GetMethod(nameof(IMaterialize.OnMaterializing));
            var onMaterializedMethod = entityType.ClrType.GetMethod(nameof(IMaterialize.OnMaterialized));

            var blockExpressions = new List<Expression>(((BlockExpression)baseExpression).Expressions);
            var instanceVariable = blockExpressions.Last() as ParameterExpression;

            var onMaterializingExpression = Expression.Call(instanceVariable, onMaterializingMethod);
            var onMaterializedExpression = Expression.Call(instanceVariable, onMaterializedMethod);

            blockExpressions.Insert(1, onMaterializingExpression);
            blockExpressions.Insert(blockExpressions.Count - 1, onMaterializedExpression);

            return Expression.Block(new[] { instanceVariable }, blockExpressions);
        }

        return baseExpression;
    }
}

Now simply replace the service while adding the DbContext to the service collection or in the Dbcontext.OnConfiguring:

services.AddDbContext<TestContext>(options =>
{
    options.ReplaceService<IEntityMaterializerSource, MyEntityMaterializerSource>();
}...

// Or...
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder.ReplaceService<IEntityMaterializerSource, MyEntityMaterializerSource>();
}

@aktxyz
Copy link

aktxyz commented Apr 29, 2020

wow ... MyEntityMaterializerSource works great :) an official approach will be great but for now this is perfect

@titobf
Copy link

titobf commented Apr 30, 2020

@StevenRasmussen thanks so much for that workaround! I have just tried it and it works!

@haiplk
Copy link

haiplk commented May 13, 2020

Hi all,,
Just curious about the override EntityMaterializerSource cannot work with nested select

dbContext.Table1.Select(f=> new CustomModel{ DateTimeField = f.DateTime }).ToListAsync()

Do you all have any idea on it

@EMooreMAC
Copy link

@StevenRasmussen just wanted to check back and say that this solution is working for us as well. Thanks a ton, you helped clear a blockage for our large scale migration to EF Core.

@smitpatel
Copy link
Contributor

@haiplk - Your query does not materialize any entity.

@Plasma
Copy link

Plasma commented Mar 8, 2021

#15911 (comment) Is a good solution, but a proper callback method being called (eg to provide an object authorisation service audit the object) would be ideal.

Are there any plans for this? Thank you

@r-work
Copy link

r-work commented Mar 10, 2021

@StevenRasmussen thanks for the workaround, however OnMaterialized gets called as soon as the "parent" entity is materialized before its eager loaded sub collections are loaded/materialized.

So unfortunately this won't work if you want a callback after the "whole" entity is loaded.

@avenmore
Copy link

avenmore commented Jun 26, 2021

It also seems that the implementation in #15911 (comment) does not call OnMaterialized() for entities fetched with .Include(). Also wish for a real solution.

Edit: My mistake - I had called .ReplaceService() on my OptionsBuilder that I use to create manual connections but not on the add services' options too. It does work for .Include().ThenInclude().

@ajcvickers
Copy link
Contributor Author

Note from triage: consider configure await when implementing this.

@ajcvickers ajcvickers modified the milestones: Backlog, 7.0.0 Oct 20, 2021
ajcvickers added a commit that referenced this issue May 30, 2022
Part of #626
Part of #15911
Fixes #20135
Fixes #14554
Fixes #24902

This is the lowest level of materialization interception--it allows the actual constructor/factory binding to be changed, such that the expression tree for the compiled delegate is altered.

Introduces singleton interceptors, which cannot be changed per context instance without re-building the internal service provider. (Note that we throw by default if this is abused and results in many service providers being created.)

The use of this for proxies has two big advantages:
- Proxy types are created lazily, which vastly improves model building time for big models with proxies. See #20135.
- Proxies can now be used with the compiled model, since the proxy types are not compiled into the model. See
ajcvickers added a commit that referenced this issue Jun 1, 2022
Part of #626
Part of #15911
Fixes #20135
Fixes #14554
Fixes #24902

This is the lowest level of materialization interception--it allows the actual constructor/factory binding to be changed, such that the expression tree for the compiled delegate is altered.

Introduces singleton interceptors, which cannot be changed per context instance without re-building the internal service provider. (Note that we throw by default if this is abused and results in many service providers being created.)

The use of this for proxies has two big advantages:
- Proxy types are created lazily, which vastly improves model building time for big models with proxies. See #20135.
- Proxies can now be used with the compiled model, since the proxy types are not compiled into the model. See
ajcvickers added a commit that referenced this issue Jun 20, 2022
Part of #626
Fixes #15911

Introduces a new `IMaterializationInterceptor` singleton interceptor that allows:
- Interception before any entity instance has been created, allowing a customized instance to be created (if desired), thereby suppressing of normal EF instance creation.
- Interception after the instance has been created, but before property values have been set. The instance can be replaced with a new instance (if desired), without preventing EF from setting property values.
- Interception before property values have been set, allowing custom setting of the values and/or suppression of setting the values by EF (if desired).
- Interception after property values have been set, allowing the instance to be changed (if desired.)

Access to property values, including shadow and service properties is provided at each point.

If no singleton materialization interceptors are configured, then the materialization delegate is the same as before, meaning any perf impact only applies if interception is used.
@ajcvickers ajcvickers added the closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. label Jun 20, 2022
ajcvickers added a commit that referenced this issue Jun 20, 2022
Part of #626
Fixes #15911

Introduces a new `IMaterializationInterceptor` singleton interceptor that allows:
- Interception before any entity instance has been created, allowing a customized instance to be created (if desired), thereby suppressing of normal EF instance creation.
- Interception after the instance has been created, but before property values have been set. The instance can be replaced with a new instance (if desired), without preventing EF from setting property values.
- Interception before property values have been set, allowing custom setting of the values and/or suppression of setting the values by EF (if desired).
- Interception after property values have been set, allowing the instance to be changed (if desired.)

Access to property values, including shadow and service properties is provided at each point.

If no singleton materialization interceptors are configured, then the materialization delegate is the same as before, meaning any perf impact only applies if interception is used.
@ajcvickers ajcvickers modified the milestones: 7.0.0, 7.0.0-preview6 Jun 23, 2022
@ajcvickers ajcvickers changed the title Implement ObjectMaterialized event Interception for object materialization (a.k.a. "ObjectMaterialized") Jul 21, 2022
@VahidN
Copy link

VahidN commented Sep 3, 2022

Is it possible to access the related DbCommand here as well? It will help to write a good caching interceptor based on this new feature.

@ajcvickers
Copy link
Contributor Author

@VahidN No; this is operating at a different level in the stack.

@ajcvickers ajcvickers modified the milestones: 7.0.0-preview6, 7.0.0 Nov 5, 2022
@maxime-poulain
Copy link

Hello,

thanks for this, it is exactly what I needed for my current development.(I am updating the IsTransient property of my entities thanks to this).
I would like just to add that the official documentation is not mentionning that this is possible thanks to IMaterializationInterceptor.

You can find that information however in the following article: What's new in EF 7.

But I guess the above link won't be the one people looking for that kind of behavior where they will be looking at.

If you could update the documentation, I am sure it would be helpfull for many people.

@yokovaski
Copy link

The official documentation is indeed lacking a reference to the new IMaterializationInterceptor. Here is the direct link to the announcement with an example of how to use it.

@ajcvickers ajcvickers removed their assignment Sep 1, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-dbcontext area-query closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. ef6-parity type-enhancement
Projects
None yet
Development

Successfully merging a pull request may close this issue.