-
Notifications
You must be signed in to change notification settings - Fork 3.2k
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
Expose a non-generic DbSet to support operations with shared-type entity types #21066
Comments
I no longer need this. I devised a clever hack to create type-free calls to extension methods like this: /// <summary>
/// A Type-free version of DbSet.FromSqlRaw
/// </summary>
/// <param name="source">A DbSet disguised as a generic IQueryable.</param>
/// <param name="sql">The sql to execute.</param>
/// <param name="parameters">The sql parameters to use.</param>
/// <returns></returns>
public static IQueryable FromSqlRaw(this IQueryable source, string sql, params object[] parameters)
{
var methodInfo = MethodInfo
.GetCurrentMethod().DeclaringType
.GetMethods(BindingFlags.NonPublic | BindingFlags.Static)
.First(m => m.Name == "Call_DbSet_FromSqlRaw");
var genericMethod = methodInfo.MakeGenericMethod(source.ElementType);
object[] newParameters = (parameters == null)
? new object[] { source, sql }
: new object[] { source, sql, parameters };
return genericMethodInfo.Invoke(source, newParameters) as IQueryable;
}
/// <summary>
/// Wrapper for an extension method I can't otherwise call without a specific type.
/// This is *only* called from the above version of FromSqlRaw through use of a MethodInfo
/// </summary>
private static IQueryable Call_DbSet_FromSqlRaw<TEntity>(DbSet<TEntity> source, string sql, params object[] parameters)
where TEntity : class
=> RelationalQueryableExtensions.FromSqlRaw(source, sql, parameters); |
If you make |
Further, |
Because of bugs in either OData or EF Core or Automapper (the latest was in EF Core - the SQL it generates is buggy and doesn't work), I was being roadblocked while developing an n-tier API I have been writing for work. So I wrote my own Uri query string parser and replaced OData entirely. I use it to:
It all works beautifully! The JSON serializer handles the In any case, to answer your question specifically, Imagine a controller method to handle a Uri like this: Using a DbContext directly with OData, the method might look something like this: [EnableQuery]
public IQueryable<Supplier> Get() => dbContext.Supplier; At design time, the only known type here is Supplier. I have no idea that I am going to need a private static IQueryable ExecuteQueries(DbContext dbContext,
QueryInfo queryInfo, params QueryInfo[] ancestors)
{
List<QueryInfo> queryInfos = new List<QueryInfo>();
string sql = null;
if (ancestors != null && ancestors.Length > 0)
{
queryInfos.AddRange(ancestors);
queryInfos.Add(queryInfo);
sql = BuildSqlQuery(queryInfos.ToArray());
}
else
{
queryInfos.Add(queryInfo);
sql = BuildSqlQuery(queryInfo);
}
Debug.WriteLine("------------------------------------------------------------");
Debug.WriteLine(sql);
Debug.WriteLine("------------------------------------------------------------");
IQueryable entitySet = GetDbSetForClrType(dbContext, queryInfo.ClrType)
.FromSqlRaw(sql,
new SqlParameter($"@{queryInfo.TableAlias}_Skip", queryInfo.Skip),
new SqlParameter($"@{queryInfo.TableAlias}_Top", queryInfo.Top));
// Eager load descendant data so that navigation properties are correctly hydrated
// when we finally iterate over the root DbSet.
// Do not iterate the parent most DbSet (where ancestors is null).
// Wait to do that until last or the database gets hit with an extra query
// when iterating over the DbSet in WalkTree().
if (ancestors != null && ancestors.Length > 0)
{
List<object> data = new List<object>();
foreach (var e in entitySet) data.Add(e);
dbContext.AttachRange(data);
}
foreach (var child in queryInfo.Expansions)
{
IQueryable childQueryable =
ExecuteQueries(dbContext, child.Value, queryInfos.ToArray());
}
return entitySet;
} As you can see, there is no TEntity type parameter to work with. I have to be able to retrieve the DbSets and execute the FromSqlRaw function at run-time knowing only the CLR Type of my navigation properties. |
The ClrType of your navigation is the type of TEntity. |
No sir, that is only true for the root most DbSet<> - <TEntity> is of Type <Supplier> only in that case. (Notice that there is only place in all that code above that a <TEntity> of a specified type is known and coded?) The ClrType of the Navigation properties is <Part>, <DistributionCenter>, <Address>, and <Contact>. How would you propose I obtain the DbSet<> for those entities at run-time when the only specified type for <TEntity> in the code is the <Supplier> parameter type from the Controller's No means exists to obtain a DbSet<> from a DbContext by name or run-time type information, unlike with EF 6. (See DbContext.Set Method). I guess some person somewhere writing the EF Core code decided that this method was of little or no value and removed it from EF Core (or never added it maybe). I'll keep my opinions about that decision to myself :-) |
FWIW, the EF Core bug I mentioned can rather easily be reproduced.
create table AppraisalYear
(
tax_yr decimal(4,0) not null,
certification_dt datetime,
constraint pk_AppraisalYear
primary key clustered (tax_yr)
)
create table PropertyYearLayer
(
owner_tax_yr decimal(4,0) not null,
sup_num int not null,
prop_id int not null,
constraint pk_PropertyYearLayer
primary key clustered (owner_tax_yr, sup_num, prop_id),
constraint fk_PropertyYearLayer_AppraisalYear
foreign key (owner_tax_yr)
references AppraisalYear(tax_yr)
)
public partial class PropertyDbContext : DbContext
{
#region Constructor(s)
public PropertyDbContext() { }
public PropertyDbContext(DbContextOptions<PropertyDbContext> options)
: base(options)
{ }
#endregion
#region DbSet<> Properties
public virtual DbSet<AppraisalYear> AppraisalYear { get; set; }
public virtual DbSet<PropertyYearLayer> PropertyYearLayer { get; set; }
#endregion
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<AppraisalYear>(OnModelCreating_AppraisalYear);
modelBuilder.Entity<PropertyYearLayer>(OnModelCreating_PropertyYearLayer);
}
#endregion
#region Fluent API Methods
private void OnModelCreating_AppraisalYear(EntityTypeBuilder<AppraisalYear> entity)
{
entity.HasKey(e => new { e.Year });
entity.ToTable("AppraisalYear");
entity.Property(e => e.Year).HasColumnName("tax_yr")
.IsRequired()
.HasColumnType("numeric(4,0)");
entity.Property(e => e.CertificationDate).HasColumnName("certification_dt")
.HasColumnType("datetime");
}
private void OnModelCreating_PropertyYearLayer(EntityTypeBuilder<PropertyYearLayer> entity)
{
entity.HasKey(e => new { e.Year, e.SupNumber, e.PropertyId });
entity.ToTable("PropertyYearLayer");
entity.Property(e => e.Year).HasColumnName("owner_tax_yr")
.IsRequired()
.HasColumnType("numeric(4,0)");
entity.Property(e => e.SupNumber).HasColumnName("sup_num")
.IsRequired();
entity.Property(e => e.PropertyId).HasColumnName("prop_id")
.IsRequired();
entity.HasOne(d => d.AppraisalYear)
.WithMany(p => p.PropertyYearLayer)
.HasForeignKey(d => d.Year);
}
#endregion
}
The result is an ugly error:
That is stemming from a bug in the SQL that EF Core generated (I ran SQL Profiler to determine this): exec sp_executesql N'SELECT [t].[tax_yr], [t].[certification_dt], [t1].[c], [t1].[owner_tax_yr], [t1].[sup_num], [t1].[prop_id], [t1].[c0]
FROM (
SELECT TOP(@__TypedProperty_4) [a].[tax_yr], [a].[certification_dt]
FROM [PACSNext].[Appraisal_Year] AS [a]
ORDER BY [a].[tax_yr] DESC
) AS [t]
OUTER APPLY (
SELECT TOP(@__TypedProperty_2) @__TypedProperty_3 AS [c], [t].[owner_tax_yr], [t].[sup_num], [t].[prop_id], CAST(1 AS bit) AS [c0]
FROM (
SELECT TOP(@__TypedProperty_1) [p].[owner_tax_yr], [p].[sup_num], [p].[prop_id]
FROM [PACSNext].[Property_Year_Layer] AS [p]
WHERE [t].[tax_yr] = [p].[owner_tax_yr]
ORDER BY [p].[prop_id], [p].[sup_num], [p].[owner_tax_yr]
) AS [t0]
ORDER BY [t].[prop_id], [t].[sup_num], [t].[owner_tax_yr]
) AS [t1]
ORDER BY [t].[tax_yr] DESC, [t1].[prop_id], [t1].[sup_num], [t1].[owner_tax_yr]',N'@__TypedProperty_4 int,@__TypedProperty_2 int,@__TypedProperty_3 nvarchar(4000),@__TypedProperty_1 int',@__TypedProperty_4=101,@__TypedProperty_2=101,@__TypedProperty_3=N'cd7f27e6-36d1-4190-91de-871916fd4c42',@__TypedProperty_1=5 The problem is the |
If you extract method info of following method, using MakeGenericMethod with whatever runtime clr type you get and invoking it will give DbSet for it. You are already doing that kind of thing for FromSql. efcore/src/EFCore/DbContext.cs Lines 283 to 284 in 95e779b
|
That makes sense. I realize now that my In any case, I hope this discussion has demonstrated that there are actually two needs for the current framework.
|
We discussed this again, but came to the same conclusion as before. Since LINQ is inherently generic for anything other than the most trivial case, we don't want to lead people down the path of trying to do things with a non--generic DbSet. If you're willing to handle the dynamic LINQ code, then calling CreateGenericMethod to create a DbSet with the generic API should not be difficult. All that being said, I'm putting this on the backlog to gather votes and feedback. |
Just a note on not being able to do much with a non-generic IQueryable: because the T in |
I need this feature, in my situation, I have to declare many validators like public class ValidDepartmentIdAttribute: ValidationAttribute
{
protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
{
if (value is not Guid id)
{
return ValidationResult.Success;
}
var departmentBusiness = validationContext.GetTypedService<IDepartmentBusiness>();
if (!departmentBusiness.ExistById(id).Result)
{
return new ValidationResult($"Element {id} can't be found");
}
return ValidationResult.Success;
}
} And I have to repeat this for all entity classes. Since attribute class doesn't support generic yet, I suppose I can write something like this one so it can be reused. public class ValidEntityIdAttribute: ValidationAttribute
{
public Type Type { get; set; }
public ValidEntityIdAttribute(Type type)
{
Type = type;
}
protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
{
if (value is not Guid id)
{
return ValidationResult.Success;
}
var dataContext = validationContext.GetTypedService<DataContext>();
if (dataContext.Set(Type).Find(id) == null)
{
return new ValidationResult($"Element {id} can't be found");
}
return ValidationResult.Success;
}
} But then I found So, I hope this method will be implemented soon. Thank you |
a nongeneric dbset instantiation mechanism would help out in savechanges interceptors. such an api surface would support enforcement of business rules that require querying navigation properties of heterogeneous entity types in the change tracker during savechanges one workaround i'm canary testing is
however even if this works and lets me filter for owners of an arbitrary entity
so this is a vote for at minimum, an extension method that
obviously, if the aforementioned extension method were pasted into a comment on this issue the party responsible would instantly attain guru status everywhere on the internet thanks for leaving the matter open to further consideration |
Assume a DbContext with two related DBSet<> properties like so:
No means exists (that I could find) to be able to the following:
I want this because I wrote my own Uri query string parser from which I use certain information to decide which parts of a DbContext to hydrate. (Consider for example, the $expand operator that OData uses. Not only is the main entity queried, but the related entities are queried as well.)
If I want more control of the SQL generated, some solution to the above would work best.
Would it be possible to add similar methods in RelationalQueryableExtensions without the strong typing?
For example:
The text was updated successfully, but these errors were encountered: