13

I have a user-defined function in SQL Server that accepts a TVP (table valued parameter) as parameter. In EF, how do I call such a function from C# ?

I tried using the method ObjectContext.CreateQuery<> but got the following error:

The parameter 'param' of function 'QueryByParam' is invalid. Parameters can only be of a type that can be converted to an Edm scalar type.

Also tried method ObjectContext.ExecuteStoreQuery<> and got the same error. It doesn't return an IQueryable anyway.

Sample code

[DbFunction(nameof(SampleDbContext), "QueryByParam")]
public IQueryable<SecurityQueryRow> QueryByParam(IEnumerable<ProfileType> profiles, bool isActive = false)
{
    DataTable dataTable = ....
    ObjectParameter profilesParam = new ObjectParameter("profileTypeIds", dataTable);

    ObjectParameter isActiveParam = new ObjectParameter("isActive ", isActive);

    return ((IObjectContextAdapter)this).ObjectContext.CreateQuery<SecurityQueryRow>(
            string.Format("[{0}].[{1}](@profileTypeIds, @isActive)", GetType().Name, "QueryByParam"),
            profilesParam,
            isActiveParam);
}

The requirement is that we need an IQueryable back, not the consumed result.

nawfal
  • 70,104
  • 56
  • 326
  • 368
Golda
  • 131
  • 1
  • 4
  • May be this information will be helpful https://entityframeworkcore.com/knowledge-base/57056492/can-ef-core-return-iqueryable-from-stored-procedure---views---table-valued-function- – Karan Sep 07 '20 at 05:47
  • We are getting return as IQueryable in normal case. But its not working when the input parameter is Tvp. For us the input parameter is a tvp(for eg;- IEnumerable). We need the output as IQueryable. – Golda Sep 07 '20 at 14:27
  • Try this information: https://www.c-sharpcorner.com/uploadfile/78607b/using-table-valued-parameters-in-entity-framework/#:~:text=Table%20Valued%20Parameters%20(TVPs)%20were,the%20records%20in%20the%20database. – MikeJ Sep 07 '20 at 18:11
  • @MikeJ, not related. Golda is asking about table valued function's case. – nawfal Sep 10 '20 at 03:59
  • @nawfal about it's case... I'm not sue what you mean by that. Do you mean he's trying type the values to something specific? The link show's use a TVP with EF. – MikeJ Sep 10 '20 at 17:15
  • 3
    I think the simple truth is that `ObjectParameter` can never be a TVP. – Gert Arnold Sep 11 '20 at 10:24
  • you should clarify what you mean by errors: are these compiler errors or runtime exceptions? (provide the exception type) also, can you clarify what EF version you are using (tag does indicate 6, but I would like some assurance on that) and what the targetFramework is? Whether this is a data first or code first project. whether your code is (or is expected to at some point) generating the user-defined function.. etc etc. – Brett Caswell Sep 11 '20 at 15:16
  • also, I do not think the query you're attempting to create is correct usage. try running\providing a snippet showing the calling of the UDF in this format: `string.Format("[{0}].[{1}](@profileTypeIds, @isActive)", GetType().Name, "QueryByParam")` .. more specifically, what do you expect `@profileTypeIds` to be (or written as)? i.e. is it `SELECT * FROM (VALUE (1,2,3)) v`or is there a shorter syntax that works and is part of the expectation? ...this really goes back to the EDM Scalar Type notion I think. something needs to describe this parameter. – Brett Caswell Sep 11 '20 at 15:27
  • I think a potentially good answer to the question would be to create a User-Defined Table Type and either modify your UDF to use it, or create a new UDF (as an overload method notion) that uses it as a parameter. ref [c-sharpcorner UDTT article](https://www.c-sharpcorner.com/article/user-defined-table-types-and-table-valued-parameters/) – Brett Caswell Sep 11 '20 at 15:39
  • @Golda, can you explain more what is your requirement and what you need to return Iquerable. simply you can create user-defined table type and make a SP Or function for that use as a parameter user-defined type. and code level pass the Datatable It'll work fine. we're using this concept in our project when we're upload data through excel to DB. – Prabhat Sep 11 '20 at 17:37

1 Answers1

9

You can do it with Raw Sql en EF Core, Similar aproach in EF6, but you can't get an IQueryable. Both examples below.

Entity Framework Core

SQL type to use it as your list filter:

CREATE TYPE [dbo].[Table1Type] AS TABLE(
    [Id] [int] NULL,
    [Name] [nchar](10) NULL
)

SQL UDF:

CREATE FUNCTION [dbo].[Func1] 
(   
    @Ids Table1Type readonly
)
RETURNS TABLE 
AS
RETURN
(
    SELECT * from Table1 where id in (select Id from @Ids)
)

EF context:

public class MyContext : DbContext
{
    public DbSet<Table1> Table1 { get; set; }
}

DTO to match the sql Type (also same as table for simplicity):

public class Table1
{
    public int Id { get; set; }
    public string Name { get; set; }
}

Example:

static void Main(string[] args)
{
    using (var context = new MyContext())
    {
        // Declare de Structure filter param
        var dt = new DataTable();

        DataTable table = new DataTable();
        table.Columns.Add(new DataColumn("Id", typeof(int)));
        table.Columns.Add(new DataColumn("Name", typeof(string)));
        DataRow row = table.NewRow();
        row["Id"] = 1;
        row["Name"] = "Item";
        table.Rows.Add(row);
        var param = new SqlParameter("@Ids", table) { TypeName = "dbo.Table1Type", SqlDbType = SqlDbType.Structured };

        IQueryable<Table1> query = context.Table1.FromSqlRaw("SELECT * FROM dbo.func1(@Ids)", param);
        var result = query.ToList();
    }
}

Entity Framework 6

You can't get an IQueryable, but you can linq to the resulting IEnumerable.

static void Main(string[] args)
{
    using (var context = new MyContext())
    {
        context.Database.Log = s => System.Diagnostics.Debug.WriteLine(s);

        // Declare de Structure filter param
        var dt = new DataTable();

        DataTable table = new DataTable();
        table.Columns.Add(new DataColumn("Id", typeof(int)));
        table.Columns.Add(new DataColumn("Name", typeof(string)));
        DataRow row = table.NewRow();
        row["Id"] = 1;
        row["Name"] = "Item";
        table.Rows.Add(row);
        var param = new SqlParameter("@Ids", table) { TypeName = "dbo.Table1Type", SqlDbType = SqlDbType.Structured };

        var query = context.Table1.SqlQuery("SELECT * FROM dbo.func1(@Ids)", param);

        var result = query.ToList();
    }
}
nerlijma
  • 935
  • 1
  • 9
  • 24
  • @golda, is this what you are looking for? Maybe you can clarify a bit if this answer won't help you. – nerlijma Sep 07 '20 at 19:59
  • 2
    This applies to EF Core, while the error message and the structure of the OP code indicates EF6. – Ivan Stoev Sep 07 '20 at 20:47
  • @IvanStoev, I edited the answer for a similar answer in EF6, however, not exactly as needed as you can't get IQueryable from SqlQuery. – nerlijma Sep 07 '20 at 22:18
  • Thanks @nerlijma. Good to know it can be done in EFCore. – nawfal Sep 10 '20 at 03:56
  • +1 for the User-Defined Table Type portion and overall functional code.. but your solution has a caveat: in ref [Raw SQL queries are useful if the query you want can't be expressed using LINQ - MSDN doc](https://learn.microsoft.com/en-us/ef/core/querying/raw-sql) your solution using `FromSqlRaw` and `SqlQuery` is not ideal. You should be able to express this entirely with LINQ; however, the query to get the User Defined Table parameter value may have to be executed before the query using user defined function. – Brett Caswell Sep 11 '20 at 16:29
  • that is, your method should take `IQueryable` (which may require data annotation of the entity model for the type), execute it and use it in an LINQ expression for User Defined Function call. avoiding any SqlClient Data Types (and\or ADO.NET specific types) – Brett Caswell Sep 11 '20 at 16:33
  • 2
    EF6 and EF Core are completely different systems. The question is specifically targeting EF6 and requesting server side `IQueryable` result, which this answer does not satisfy. If you can't satisfy the requirements, the correct answer is simply "Not possible". – Ivan Stoev Sep 12 '20 at 18:15