igor-tkachev / bltoolkit

Business Logic Toolkit for .NET
MIT License
297 stars 112 forks source link

BLToolkit fuction table-valued parameter in linq #348

Closed avyrupaev closed 9 years ago

avyrupaev commented 9 years ago

I want to invoke table-valued fuction with table-valued parameter in SqlServer 2008 from BLToolkit linq query. How can I do it?

Sql functions is:

CREATE FUNCTION [dbo].[saf_get_ids_from_data_table]
(
    @DataTable IDsTable READONLY
)
RETURNS TABLE 
AS
RETURN 
(
    SELECT ID
    FROM @DataTable
)

SqlServer user-defined table type is:

CREATE TYPE [dbo].[IDsTable] AS TABLE 
(
    ID INT PRIMARY KEY NOT NULL
)

I tried declare it in this way:

public class TableDbFunctions
{
        private readonly DbManager _db;

        public TableDbFunctions(DbManager db)
        {
            _db = db;
        }

        [TableFunction(Name = "dbo.saf_get_ids_from_data_table")]
        public Table<IdXmlEntity> GetIdsFromDataTable(int[] ids)
        {
            return _db.GetTable<IdXmlEntity>(this, (MethodInfo)(MethodBase.GetCurrentMethod()), ids);
        }

        //or this way
        [TableFunction(Name = "dbo.saf_get_ids_from_data_table")]
        public Table<IdXmlEntity> GetIdsFromDataTable([ParamTypeName("IDsTable")]int[] ids)
        {
            return _db.GetTable<IdXmlEntity>(this, (MethodInfo)(MethodBase.GetCurrentMethod()), ids);
        }
}

public class IdXmlEntity
{
     public int ID { get; set; }
}

But catch ArgumentException "The table type parameter '@p1' must have a valid type name."

This question on stackoverflow

avyrupaev commented 9 years ago

Found solution myself.

You need to change default Sql2008DataProvider like this:

public sealed class Sql2008WithTablesDataProvider : SqlDataProviderBase
{
    static readonly List<Func<Type, string>> _udtTypeNameResolvers = new List<Func<Type, string>>();

    static Sql2008WithTablesDataProvider()
    {
        AddUdtTypeNameResolver(ResolveStandartUdt);
    }

    public static void AddUdtTypeNameResolver(Func<Type, string> resolver)
    {
        _udtTypeNameResolvers.Add(resolver);
    }

    static string ResolveStandartUdt(Type type)
    {
        return type.Namespace == "Microsoft.SqlServer.Types" ? type.Name.Replace("Sql", "") : null;
    }

    public override string Name
    {
        get { return "Sql2008WithTables"; }
    }

    public override ISqlProvider CreateSqlProvider()
    {
        return new MsSql2008SqlProvider();
    }

    public override void SetParameterValue(IDbDataParameter parameter, object value)
    {
        base.SetParameterValue(parameter, value);
        SetUdtTypeName(parameter, value);
        SetTypeName(parameter, value);
    }

    static void SetUdtTypeName(IDbDataParameter parameter, object value)
    {
        var sqlParameter = parameter as System.Data.SqlClient.SqlParameter;
        var valueType = value.GetType();

        if (sqlParameter != null)
            sqlParameter.UdtTypeName = _udtTypeNameResolvers.Select(_ => _(valueType)).FirstOrDefault(_ => !string.IsNullOrEmpty(_));
    }

    static void SetTypeName(IDbDataParameter parameter, object value)
    {
        var sqlParameter = parameter as SqlParameter;

        if (value.GetType() == typeof(DataTable))
            sqlParameter.TypeName = (string)((DataTable)value).ExtendedProperties["TypeName"];
    }
}

Convert your collection to DataTable like this:

public static DataTable ToDataTable<T>(this IEnumerable<T> items, string typeName)
{
    var tb = new DataTable(typeof(T).Name);

    tb.ExtendedProperties.Add("TypeName", typeName);

    var props = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);

    foreach (var prop in props)
    {
        tb.Columns.Add(prop.Name, prop.PropertyType);
    }

    foreach (var item in items)
    {
        var values = new object[props.Length];
        for (var i = 0; i < props.Length; i++)
        {
            values[i] = props[i].GetValue(item, null);
        }

        tb.Rows.Add(values);
    }

    return tb;
}

Declare function as

public class TableDbFunctions
{
        private readonly DbManager _db;

        public TableDbFunctions(DbManager db)
        {
            _db = db;
        }

        [TableFunction(Name = "dbo.saf_get_ids_from_data_table")]
        public Table<IdEntity> GetIdsFromDataTable(DataTable table)
        {
            return _db.GetTable<IdEntity>(this, (MethodInfo)(MethodBase.GetCurrentMethod()), table);
        }
}

public class IdEntity
{
     public int ID { get; set; }
}

and invoke

using (var db = new DbManager(new Sql2008WithTablesDataProvider(), yourConnectionString))
{
    new ColumnDbFunctions(db).GetIdsFromDataTable(numbers.Select(n => new IdEntity() { ID = n }).ToDataTable("dbo.IDsTable"));
}

Profit!