Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@ public CustomQueryCompiler(IQueryCompiler decoratedQueryCompiler,
var trackingByDefault = (contextOptions.FindExtension<CoreOptionsExtension>()?.QueryTrackingBehavior ?? QueryTrackingBehavior.TrackAll) ==
QueryTrackingBehavior.TrackAll;

_projectableExpressionReplacer = new ProjectableExpressionReplacer(new ProjectionExpressionResolver(), trackingByDefault);
var populateSettableProperties = contextOptions.FindExtension<ProjectionOptionsExtension>()?.PopulateSettableProperties ?? true;

_projectableExpressionReplacer = new ProjectableExpressionReplacer(new ProjectionExpressionResolver(), trackingByDefault, populateSettableProperties);
}

public override Func<QueryContext, TResult> CreateCompiledAsyncQuery<TResult>(Expression query)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ namespace EntityFrameworkCore.Projectables.Infrastructure.Internal
public class ProjectionOptionsExtension : IDbContextOptionsExtension
{
CompatibilityMode _compatibilityMode = CompatibilityMode.Full;
bool _populateSettableProperties = true;

public ProjectionOptionsExtension()
{
Expand Down Expand Up @@ -91,6 +92,21 @@ static object CreateTargetInstance(IServiceProvider services, ServiceDescriptor
}
}

/// <summary>
/// Whether projectable properties that have a setter are filled in with their
/// database-computed value when a query loads whole entities. Enabled by default.
/// </summary>
public bool PopulateSettableProperties => _populateSettableProperties;

public ProjectionOptionsExtension WithPopulateSettableProperties(bool enabled)
{
var clone = Clone();

clone._populateSettableProperties = enabled;

return clone;
}

public ProjectionOptionsExtension WithCompatibilityMode(CompatibilityMode compatibilityMode)
{
var clone = Clone();
Expand Down Expand Up @@ -126,13 +142,15 @@ public override void PopulateDebugInfo(IDictionary<string, string> debugInfo)
}

debugInfo["Projectables:CompatibilityMode"] = Extension._compatibilityMode.ToString();
debugInfo["Projectables:PopulateSettableProperties"] = Extension._populateSettableProperties.ToString();
}

public override int GetServiceProviderHashCode()
{
var hashCode = new HashCode();

hashCode.Add(Extension._compatibilityMode);
hashCode.Add(Extension._populateSettableProperties);

return hashCode.ToHashCode();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,20 @@ public ProjectableOptionsBuilder(DbContextOptionsBuilder optionsBuilder)
public ProjectableOptionsBuilder CompatibilityMode(CompatibilityMode mode)
=> WithOption(x => x.WithCompatibilityMode(mode));

/// <summary>
/// Controls whether projectable properties that have a setter are filled in with their
/// database-computed value when a query loads whole entities (issue #84). Enabled by
/// default.
/// <para>
/// Disabling it leaves projectables usable inside queries -- in Select, Where,
/// OrderBy and so on -- while a loaded entity keeps whatever its CLR member
/// returns. That is the behaviour of versions from before #84, and is what you want
/// when entities are only ever projected explicitly.
/// </para>
/// </summary>
public ProjectableOptionsBuilder PopulateSettableProperties(bool enabled)
=> WithOption(x => x.WithPopulateSettableProperties(enabled));

/// <summary>
/// Sets an option by cloning the extension used to store the settings. This ensures the builder
/// does not modify options that are already in use elsewhere.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public sealed class ProjectableExpressionReplacer : ExpressionVisitor
private IQueryProvider? _currentQueryProvider;
private bool _disableRootRewrite = false;
private readonly bool _trackingByDefault;
private readonly bool _populateSettableProperties;
private IEntityType? _entityType;

// Extract MethodInfo via expression trees (trim-safe; computed once per AppDomain)
Expand All @@ -38,9 +39,10 @@ public sealed class ProjectableExpressionReplacer : ExpressionVisitor
private readonly static ConditionalWeakTable<Type, MethodInfo> _closedSelectCache = new();
private readonly static ConditionalWeakTable<Type, MethodInfo> _closedWhereCache = new();

public ProjectableExpressionReplacer(IProjectionExpressionResolver projectionExpressionResolver, bool trackByDefault = false)
public ProjectableExpressionReplacer(IProjectionExpressionResolver projectionExpressionResolver, bool trackByDefault = false, bool populateSettableProperties = true)
{
_trackingByDefault = trackByDefault;
_populateSettableProperties = populateSettableProperties;
_resolver = projectionExpressionResolver;
}

Expand Down Expand Up @@ -69,6 +71,14 @@ bool TryGetReflectedExpression(MemberInfo memberInfo, [NotNullWhen(true)] out La

var ret = Visit(node);

if (!_populateSettableProperties)
{
// Opted out via UseProjectables(o => o.PopulateSettableProperties(false)).
// Projectables are still expanded wherever a query reads them; they are just
// not assigned onto materialized entities.
return ret;
}

if (_disableRootRewrite)
{
// This boolean is enabled when a "Select" is encountered
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,21 @@ public class SampleDbContext<TEntity> : DbContext
{
readonly CompatibilityMode _compatibilityMode;
readonly QueryTrackingBehavior _queryTrackingBehavior;
readonly bool _populateSettableProperties;

public SampleDbContext(CompatibilityMode compatibilityMode = CompatibilityMode.Full, QueryTrackingBehavior queryTrackingBehavior = QueryTrackingBehavior.TrackAll)
public SampleDbContext(CompatibilityMode compatibilityMode = CompatibilityMode.Full, QueryTrackingBehavior queryTrackingBehavior = QueryTrackingBehavior.TrackAll, bool populateSettableProperties = true)
{
_compatibilityMode = compatibilityMode;
_queryTrackingBehavior = queryTrackingBehavior;
_populateSettableProperties = populateSettableProperties;
}

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer("Server=(localdb)\\v11.0;Integrated Security=true"); // Fake connection string as we're actually never connecting
optionsBuilder.UseProjectables(options => {
options.CompatibilityMode(_compatibilityMode); // Needed by our ComplexModelTests
options.PopulateSettableProperties(_populateSettableProperties);
});
optionsBuilder.UseQueryTrackingBehavior(_queryTrackingBehavior);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
SELECT [e].[Id]
FROM [Entity] AS [e]
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
SELECT [e].[Id]
FROM [Entity] AS [e]
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
SELECT [e].[Id]
FROM [Entity] AS [e]
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
SELECT [e].[Id] * 5 AS [ComputedWithBacking]
FROM [Entity] AS [e]
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
SELECT [e].[Id] * 5 AS [ComputedWithBacking]
FROM [Entity] AS [e]
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
SELECT [e].[Id] * 5 AS [ComputedWithBacking]
FROM [Entity] AS [e]
Original file line number Diff line number Diff line change
Expand Up @@ -76,5 +76,33 @@ public Task AsNoTrackingQueryRootExpression()

return Verifier.Verify(query.ToQueryString());
}

// PopulateSettableProperties(false) skips the query root rewrite entirely, so a
// materialized entity keeps whatever its CLR member returns. Projectables are still
// expanded wherever a query reads them; see
// PopulateSettablePropertiesDisabledStillExpandsInProjection below.
[Fact]
public Task PopulateSettablePropertiesDisabledQueryRootExpression()
{
using var dbContext = new SampleDbContext<Entity>(
queryTrackingBehavior: QueryTrackingBehavior.NoTracking,
populateSettableProperties: false);

var query = dbContext.Set<Entity>();

return Verifier.Verify(query.ToQueryString());
}

[Fact]
public Task PopulateSettablePropertiesDisabledStillExpandsInProjection()
{
using var dbContext = new SampleDbContext<Entity>(
queryTrackingBehavior: QueryTrackingBehavior.NoTracking,
populateSettableProperties: false);

var query = dbContext.Set<Entity>().Select(e => new { e.ComputedWithBacking });

return Verifier.Verify(query.ToQueryString());
}
}
}