25

我有一个InsertCars接受用户定义表类型列表的存储过程CarType

CREATE TYPE dbo.CarType
AS TABLE
(
    CARID int null,
    CARNAME varchar(800) not null,
);

CREATE PROCEDURE dbo.InsertCars
    @Cars AS CarType READONLY
AS
-- RETURN COUNT OF INSERTED ROWS
END

我需要从 Dapper 调用这个存储过程。我用谷歌搜索并找到了一些解决方案。

 var param = new DynamicParameters(new{CARID= 66, CARNAME= "Volvo"});

 var result = con.Query("InsertCars", param, commandType: CommandType.StoredProcedure);

但我收到一个错误:

过程或函数 InsertCars 指定的参数过多

存储过程还InsertCars返回插入行的计数;我需要得到这个值。

问题的根源在哪里?

我的问题也是我在通用列表中有汽车,List<Car> Cars我想将此列表传递给存储过程。它存在优雅的方式怎么办?

public class Car
{
    public CarId { get; set; }
    public CarName { get; set; }
}

谢谢你的帮助

已编辑

我找到了解决方案

Dapper 是否支持 SQL 2008 表值参数?

或者

Dapper 是否支持 SQL 2008 表值参数 2?

所以我尝试制作自己的愚蠢助手类

class CarDynamicParam : Dapper.SqlMapper.IDynamicParameters
{
    private Car car;

    public CarDynamicParam(Car car)
    {
        this.car = car;
    }

    public void AddParameters(IDbCommand command, SqlMapper.Identity identity)
    {
        var sqlCommand = (SqlCommand)command;

        sqlCommand.CommandType = CommandType.StoredProcedure;

        var carList = new List<Microsoft.SqlServer.Server.SqlDataRecord>();

        Microsoft.SqlServer.Server.SqlMetaData[] tvpDefinition =
                                                                {

                                                                    new Microsoft.SqlServer.Server.SqlMetaData("CARID", SqlDbType.Int),
                                                                    new Microsoft.SqlServer.Server.SqlMetaData("CARNAME", SqlDbType.NVarChar, 100),
                                                                };

        var rec = new Microsoft.SqlServer.Server.SqlDataRecord(tvpDefinition);
        rec.SetInt32(0, car.CarId);
        rec.SetString(1, car.CarName);

        carList.Add(rec);

        var p = sqlCommand.Parameters.Add("Cars", SqlDbType.Structured);
        p.Direction = ParameterDirection.Input;
        p.TypeName = "CarType";
        p.Value = carList;
    }
}

采用

var result = con.Query("InsertCars", new CarDynamicParam(car), commandType: CommandType.StoredProcedure);

我得到异常

使用多映射 API 时,如果您有除 Id 以外的键,请确保设置 splitOn 参数。

堆栈跟踪:

   at Dapper.SqlMapper.GetDynamicDeserializer(IDataRecord reader, Int32 startBound, Int32 length, Boolean returnNullIfFirstMissing) in c:\Dev\Dapper\Dapper\SqlMapper.cs:line 1308
   at Dapper.SqlMapper.GetDeserializer(Type type, IDataReader reader, Int32 startBound, Int32 length, Boolean returnNullIfFirstMissing) in c:\Dev\Dapper\Dapper\SqlMapper.cs:line 1141
   at Dapper.SqlMapper.<QueryInternal>d__d`1.MoveNext() in c:\Dev\Dapper\Dapper\SqlMapper.cs:line 819
   at System.Collections.Generic.List`1..ctor(IEnumerable`1 collection)
   at System.Linq.Enumerable.ToList[TSource](IEnumerable`1 source)
   at Dapper.SqlMapper.Query[T](IDbConnection cnn, String sql, Object param, IDbTransaction transaction, Boolean buffered, Nullable`1 commandTimeout, Nullable`1 commandType) in c:\Dev\Dapper\Dapper\SqlMapper.cs:line 770
   at Dapper.SqlMapper.Query(IDbConnection cnn, String sql, Object param, IDbTransaction transaction, Boolean buffered, Nullable`1 commandTimeout, Nullable`1 commandType) in c:\Dev\Dapper\Dapper\SqlMapper.cs:line 715

怎么了?

固定的:

con.Execute改为打电话con.Query

4

7 回答 7

20

我的问题也是我在通用列表 List Cars 中有汽车,我想将此列表传递给存储过程。它存在优雅的方式怎么办?

您需要将通用列表 Car 转换为数据表,然后将其传递给存储过程。需要注意的一点是,您的字段顺序必须与数据库中用户定义的表类型中定义的顺序相同。否则数据将无法正确保存。它也必须具有相同数量的列

我使用这种方法将 List 转换为 DataTable。您可以将其称为 yourList.ToDataTable()

public static DataTable ToDataTable<T>(this List<T> iList)
    {
        DataTable dataTable = new DataTable();
        PropertyDescriptorCollection propertyDescriptorCollection =
            TypeDescriptor.GetProperties(typeof(T));
        for (int i = 0; i < propertyDescriptorCollection.Count; i++)
        {
            PropertyDescriptor propertyDescriptor = propertyDescriptorCollection[i];
            Type type = propertyDescriptor.PropertyType;

            if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
                type = Nullable.GetUnderlyingType(type);


            dataTable.Columns.Add(propertyDescriptor.Name, type);
        }
        object[] values = new object[propertyDescriptorCollection.Count];
        foreach (T iListItem in iList)
        {
            for (int i = 0; i < values.Length; i++)
            {
                values[i] = propertyDescriptorCollection[i].GetValue(iListItem);
            }
            dataTable.Rows.Add(values);
        }
        return dataTable;
    }
于 2013-08-16T09:27:57.513 回答
18

我知道这有点老了,但我想我还是会在这上面发帖,因为我试图让这更容易一些。我希望我已经使用我创建的 NuGet 包做到了这一点,该包将允许以下代码:

public class CarType
{
  public int CARID { get; set; }
  public string CARNAME{ get; set; }
}

var cars = new List<CarType>{new CarType { CARID = 1, CARNAME = "Volvo"}};

var parameters = new DynamicParameters();
parameters.AddTable("@Cars", "CarType", cars)

 var result = con.Query("InsertCars", parameters, commandType: CommandType.StoredProcedure);

NuGet 包:https ://www.nuget.org/packages/Dapper.ParameterExtensions/0.2.0 仍处于早期阶段,因此可能无法与所有内容一起使用!

请阅读自述文件并随时在 GitHub 上贡献:https ://github.com/RasicN/Dapper-Parameters

于 2016-12-28T18:13:01.650 回答
2

您也可以使用 Query 方法以及 Execute。参数必须是 DataTable 类型,并且可以作为匿名参数集合的一部分提供。如果您的需求很简单,并且不需要通用解决方案来构建 DataTable,那么一个小的非通用函数是最简单的:

private DataTable CreateDataTable( IEnumerable<AlertChannelContainer> alertData )
{
    DataTable table = new DataTable();
    table.Columns.Add( "ChannelOrdinal", typeof( int ) );
    table.Columns.Add( "Value", typeof( decimal ) );

    foreach ( var alertChannel in alertData )
    {
        var dataRow = table.NewRow();
        dataRow["ChannelOrdinal"] = alertChannel.ChannelOrdinal;
        dataRow["Value"] = alertChannel.Value;
            table.Rows.Add( dataRow );
    }

    return table;
}

然后像这样调用它:

var result = await connection.QuerySingleAsync<AlertMetadata>( 
    "[dbo].[InsertAlert]",
    new
    {
        eventId,
        deviceId,
        timestamp,
        alertThresholds = JsonConvert.SerializeObject( rules ),
        data = CreateDataTable( alertData )
    },
    commandType: CommandType.StoredProcedure );
于 2020-06-17T09:39:00.790 回答
1

使用反射将对象属性映射到数据表列是昂贵的。进一步采用 Ehsan 的解决方案,在关注性能的情况下,您可以缓存类型属性映射。正如 Ehsan 还指出的,类中的顺序必须与数据库中的顺序相同,并且列数必须相等。这可以通过根据类型定义重新排序列来克服。

public static class DataTableExtensions
{
    private static readonly EntityPropertyTypeMap PropertyTypeMap = new EntityPropertyTypeMap();

    public static DataTable ToDataTable<T>(this ICollection<T> values)
    {
        if (values is null)
        {
            throw new ArgumentNullException(nameof(values));
        }

        var table = new DataTable();

        var properties = PropertyTypeMap.GetPropertiesForType<T>().Properties;

        foreach (var prop in properties)
        {
            table.Columns.Add(prop.Name, Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType);
        }

        foreach (var value in values)
        {
            var propertyCount = properties.Count();
            var propertyValues = new object[propertyCount];

            if (value != null)
            {
                for (var i = 0; i < propertyCount; i++)
                {
                    propertyValues[i] = properties[i].GetValue(value);
                }
            }

            table.Rows.Add(propertyValues);
        }

        return table;
    }
}


public static class DapperExtensions
{
    private static readonly SqlSchemaInfo SqlSchemaInfo = new SqlSchemaInfo();

    public static DataTable ConvertCollectionToUserDefinedTypeDataTable<T>(this SqlConnection connection, ICollection<T> values, string dataTableType = null)
    {
        if (dataTableType == null)
        {
            dataTableType = typeof(T).Name;
        }

        var data = values.ToDataTable();

        data.TableName = dataTableType;

        var typeColumns = SqlSchemaInfo.GetUserDefinedTypeColumns(connection, dataTableType);

        data.SetColumnsOrder(typeColumns);

        return data;
    }

    public static DynamicParameters AddTableValuedParameter(this DynamicParameters source, string parameterName, DataTable dataTable, string dataTableType = null)
    {
        if (dataTableType == null)
        {
            dataTableType = dataTable.TableName;
        }

        if (dataTableType == null)
        {
            throw new NullReferenceException(nameof(dataTableType));
        }

        source.Add(parameterName, dataTable.AsTableValuedParameter(dataTableType));

        return source;
    }

    private static void SetColumnsOrder(this DataTable table, params string[] columnNames)
    {
        int columnIndex = 0;

        foreach (var columnName in columnNames)
        {
            table.Columns[columnName].SetOrdinal(columnIndex);
            columnIndex++;
        }
    }
}

class EntityPropertyTypeMap
{
    private readonly ConcurrentDictionary<Type, TypePropertyInfo> _mappings;

    public EntityPropertyTypeMap()
    {
        _mappings = new ConcurrentDictionary<Type, TypePropertyInfo>();
    }

    public TypePropertyInfo GetPropertiesForType<T>()
    {
        var type = typeof(T);
        return GetPropertiesForType(type);
    }

    private TypePropertyInfo GetPropertiesForType(Type type)
    {
        return _mappings.GetOrAdd(type, (key) => new TypePropertyInfo(type));
    }
}


class TypePropertyInfo
{
    private readonly Lazy<PropertyInfo[]> _properties;
    public PropertyInfo[] Properties => _properties.Value;

    public TypePropertyInfo(Type objectType)
    {
        _properties = new Lazy<PropertyInfo[]>(() => CreateMap(objectType), true);
    }

    private PropertyInfo[] CreateMap(Type objectType)
    {
        var typeProperties = objectType
            .GetProperties(BindingFlags.DeclaredOnly |
                           BindingFlags.Public |
                           BindingFlags.Instance)
            .ToArray();

        return typeProperties.Where(property => !IgnoreProperty(property)).ToArray();
    }

    private static bool IgnoreProperty(PropertyInfo property)
    {
        return property.SetMethod == null || property.GetMethod.IsPrivate || HasAttributeOfType<IgnorePropertyAttribute>(property);
    }

    private static bool HasAttributeOfType<T>(MemberInfo propInfo)
    {
        return propInfo.GetCustomAttributes().Any(a => a is T);
    }
}

public class SqlSchemaInfo
{
    private readonly ConcurrentDictionary<string, string[]> _udtColumns = new ConcurrentDictionary<string, string[]>();

    public string[] GetUserDefinedTypeColumns(SqlConnection connection, string dataTableType)
    {
        return _udtColumns.GetOrAdd(dataTableType, (x) =>
            connection.Query<string>($@"
                    SELECT name FROM 
                    (
                        SELECT column_id, name
                        FROM sys.columns
                        WHERE object_id IN (
                          SELECT type_table_object_id
                          FROM sys.table_types
                          WHERE name = '{dataTableType}'
                        )
                    ) Result
                    ORDER BY column_id").ToArray());
    }
}


[AttributeUsage(AttributeTargets.Property)]
public sealed class IgnorePropertyAttribute : Attribute
{

}
于 2019-10-23T12:33:25.990 回答
0

另一种解决方案是这样称呼它

var param = new DynamicParameters(new{CARID= 66, CARNAME= "Volvo"});

var result = con.Query<dynamic>("InsertCars", param);

删除:新 CarDynamicParam(car),commandType:CommandType.StoredProcedure

直接使用表类型的参数,就可以了。

如果可以使用Datatable(.net core 不支持),那就很简单了。

创建 DataTable -> 添加所需的列以匹配您的表类型 -> 添加所需的行。最后只需像这样使用 dapper 调用它。

var result = con.Query<dynamic>("InsertCars", new{paramFromStoredProcedure=yourDataTableInstance}, commandType: CommandType.StoredProcedure);
于 2016-09-21T23:56:30.683 回答
0

添加到上面的 JCisar 结果中,您可以创建一个通用函数,该函数既可用于单个参数,也可在此处列出。您可以将返回类型设置为您的预期结果。

调用它你可以做

var cars = new List<CarType>{new CarType { CARID = 1, CARNAME = "Volvo"}};
var parameters = new DynamicParameters();
parameters.AddTable("@Cars", "CarType", cars)

//调用函数

ExecuteStordProcedureDynamic("ProcedureName", parameters)


public IEnumerable<T> ExecuteStordProcedureDynamic<T>(String ProcedureName, DynamicParameters Param) where T : class
        {
            using (SqlConnection conn = new SqlConnection(ConnectionString))
            {
                conn.Open();

                var result= conn.Query<T>(ProcedureName, Param, commandType: CommandType.StoredProcedure);
                return result;
            }
        }
于 2020-05-16T08:08:36.170 回答
0

添加到上面的 Ehsan & Pawan Nepal 答案中,我想出了一个适用于 .NET Core 和 .NET 的解决方案。以下是执行此操作的步骤。

1.创建扩展方法将模型转换为数据表(感谢Ehsan提供上述扩展方法)。

public static DataTable ToDataTable<T>(this List<T> iList)
    {
        DataTable dataTable = new DataTable();
        PropertyDescriptorCollection propertyDescriptorCollection =
            TypeDescriptor.GetProperties(typeof(T));
        for (int i = 0; i < propertyDescriptorCollection.Count; i++)
        {
            PropertyDescriptor propertyDescriptor = propertyDescriptorCollection[i];
            Type type = propertyDescriptor.PropertyType;

            if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
                type = Nullable.GetUnderlyingType(type);


            dataTable.Columns.Add(propertyDescriptor.Name, type);
        }
        object[] values = new object[propertyDescriptorCollection.Count];
        foreach (T iListItem in iList)
        {
            for (int i = 0; i < values.Length; i++)
            {
                values[i] = propertyDescriptorCollection[i].GetValue(iListItem);
            }
            dataTable.Rows.Add(values);
        }
        return dataTable;
    }

2.将模型转换为数据表。

var cars = new List<CarType>{new CarType { CARID = 1, CARNAME = "Volvo"}};
DataTable dt = cars.ToDataTable();

3.最后添加UDT作为动态参数并将其传递给查询

 var parameters = new DynamicParameters();
 parameters.Add("@paramFromStoredProcedure", 
 dt.AsTableValuedParameter("YourUDTNameInDataBase"));
 //Pass it to query
var result = con.Query<dynamic>("InsertCars", parameters , commandType: CommandType.StoredProcedure);
于 2021-06-02T20:42:56.243 回答