SQL Server CLR Int64 to SQLInt64 Specified cast is not valid - c#

I am trying to write a CLR that allows me to run a WMI Query on a SQL Server.
using System;
using System.Data.Sql;
using Microsoft.SqlServer.Server;
using System.Collections;
using System.Data.SqlTypes;
using System.Diagnostics;
using System.Management;
public class WMIQuery
{
[SqlFunction(FillRowMethodName = "FillRow")]
public static IEnumerable InitMethod()
{
ManagementScope scope = new ManagementScope();
scope = new ManagementScope(#"\\localhost\root\CIMV2");
scope.Connect();
SelectQuery query = new SelectQuery("SELECT Name, Capacity, Freespace FROM Win32_Volume WHERE DriveType=3");
ManagementObjectSearcher searcher = new ManagementObjectSearcher(scope, query);
ManagementObjectCollection retObjectCollection = searcher.Get ( );
return retObjectCollection;
}
public static void FillRow(Object obj, out SqlString Name, out SqlInt64 Capacity, out SqlInt64 Freespace)
{
ManagementObject m = (ManagementObject)obj;
Name = new SqlString((string)m["name"]);
Capacity = new SqlInt64((Int64)m["Capacity"]);
Freespace = new SqlInt64((Int64)m["Freespace"]);
}
}
When running that table valued function i get the following error:
An error occurred while getting new row from user defined Table Valued
Function : System.InvalidCastException: Specified cast is not valid.
System.InvalidCastException: at WMIQuery.FillRow(Object obj,
SqlString& Name, SqlInt64& Capacity, SqlInt64& Freespace) .
I already found out that the problem is the the conversion:
Capacity = new SqlInt64((Int64)m["Capacity"]);
Freespace = new SqlInt64((Int64)m["Freespace"]);
I hope that someone has an idea how to solve the above problem?
My code to test this CLR is:
CREATE FUNCTION [dbo].[WMIQuery]()
RETURNS TABLE (
[Name] [nvarchar](4000) NULL,
[Capacity] [bigint] NULL,
[Freespace] [bigint] NULL
) WITH EXECUTE AS CALLER
AS
EXTERNAL NAME [MyFirstAssembly].[WMIQuery].[InitMethod]
GO
select * from WMIQuery()

You should use and check whether that row and column has proper values which can be converted to the Int64 or not. Try how to check this Here.
Please do the following before casting
bool success = Int64.TryParse(Convert.ToString(m["Capacity"]), out long number);
if (success)
{
Capacity = new SqlInt64((Int64)m["Capacity"]);
}
else
{
Capacity = 0;
}

The type of Disk m["Capacity"] is UInt64.
I've used this function to find out what data type was used.
m["Capacity"].GetType().ToString();
I've modifided the CLR to output just the datatype for that purpose.
After knowing the type i've done some research how to convert a UInt64 to Int64 and finally found the soultion:
Int64 int64cap;
Int64.TryParse(m["Capacity"].ToString(), out int64cap);
I don't know if this is the proper solution, but it works for me.
Here's the complete code
public class WMIQuery
{
[SqlFunction(FillRowMethodName = "FillRow")]
public static IEnumerable InitMethod()
{
ManagementScope scope = new ManagementScope();
scope = new ManagementScope(#"\\localhost\root\CIMV2");
scope.Connect();
SelectQuery query = new SelectQuery("SELECT Name, Capacity, Freespace FROM Win32_Volume WHERE DriveType=3");
ManagementObjectSearcher searcher = new ManagementObjectSearcher(scope, query);
ManagementObjectCollection retObjectCollection = searcher.Get ( );
return retObjectCollection;
}
public static void FillRow(Object obj, out SqlString Name, out SqlDecimal Capacity, out SqlDecimal Freespace)
{
ManagementObject m = (ManagementObject)obj;
Name = new SqlString((string)m["name"]);
Int64 int64cap;
Int64.TryParse(m["Capacity"].ToString(), out int64cap);
decimal decCap;
decCap = int64cap / 1073741824; // to GB
decCap = Math.Round(decCap, 2);
Capacity = new SqlDecimal(decCap);
Int64 int64Free;
Int64.TryParse(m["Freespace"].ToString(), out int64Free);
decimal decFree;
decFree = int64Free / 1073741824; // to GB
decFree = Math.Round(decFree, 2);
Freespace = new SqlDecimal(decFree);
}
}
The SQL to run this stuff:
CREATE ASSEMBLY [MyFirstAssembly]
FROM 'C:\MyFirstAssembly.dll'
WITH PERMISSION_SET = UNSAFE
GO
CREATE FUNCTION [dbo].[WMIQuery]()
RETURNS TABLE (
[Name] [nvarchar](4000) NULL,
[Capacity] decimal(18,2) NULL,
[Freespace] decimal(18,2) NULL
) WITH EXECUTE AS CALLER
AS
EXTERNAL NAME [MyFirstAssembly].[WMIQuery].[InitMethod]
GO
select * from WMIQuery()

As you found out, the error is due to m["Capacity"] being an unsigned, not signed, Int64. To fix, just use the Convert class as follows:
Capacity = new SqlInt64(Convert.ToInt64(m["Capacity"]));
Freespace = new SqlInt64(Convert.ToInt64(m["Freespace"]));
I tested this with your code, got the same error before making any changes, and then made the change recommended above and I now get the correct output.
While not part of the problem here but just in general (initially there was a String Querytext input param): For input parameters / return types, please use Sql* types instead of native types for most datatypes (object for SQL_VARIANT and DateTime for DATETIME2 are notable exceptions). So, use SqlString instead of String for the Querytext parameter (or just remove the input parameter as it is not being used). You can get the native .NET string out of the parameter by using the Value property (e.g. Querytext.Value), which all Sql* types have (which returns the expected native type in each case).
For more info on using SQLCLR in general, please visit: SQLCLR Info
HOWEVER, and possibly more importantly: looking at exactly what you are querying via WMI, it looks like you are getting info that there is already a DMV for, sys.dm_os_volume_stats. You could get the exact same info, for all drives / volumes that you already have files on by using the following query:
SELECT DISTINCT vol.[volume_mount_point], vol.[volume_id], vol.[logical_volume_name],
vol.[file_system_type], vol.[total_bytes], vol.[available_bytes],
(vol.[total_bytes] - vol.[available_bytes]) AS [used_bytes]
FROM sys.master_files files
CROSS APPLY sys.dm_os_volume_stats(files.[database_id], files.[file_id]) vol

Related

Npgsql C# - pass parameters as array of composite type into stored procedure

Maybe this topic is duplicated from this Array of composite type as stored procedure input passed by C# Npgsql. But that is old one from 2017 and some APIs, properties are deprecated.
Currently, I am trying to pass an array of composite types to the stored procedures. I do map a globally composite type. But there an exception was thrown Can't write CLR type Web.API.Models.UdtSpParameter[] with handler type MappedCompositeHandler`1
I try to google that seems to not find any result to resolve that. The following below that what I create, mapping, calling command stored procedure.
Postgresql
/* Create composite type */
CREATE TYPE udt_sp_parameter AS (
field VARCHAR(50),
value VARCHAR
);
/* Create stored procedure */
CREATE OR REPLACE PROCEDURE stored_example(
parameters udt_sp_parameter[])
LANGUAGE 'plpgsql'
AS $BODY$
DECLARE
_refCursor CURSOR FOR SELECT field, value FROM UNNEST(parameters::udt_sp_parameter[]);
_record udt_sp_parameter;
BEGIN
OPEN _refCursor;
LOOP
FETCH _refCursor INTO _record;
IF NOT FOUND THEN
EXIT;
END IF;
RAISE NOTICE 'Field: %', _record.field;
RAISE NOTICE 'Value: %', _record.value IS NULL;
END LOOP;
CLOSE _refCursor;
END;
$BODY$;
And I try to call the stored by plpgsql language and work well.
DO
$$
DECLARE
parameters udtt_input_param[] := ARRAY[
ROW('YED','Yeti')
,ROW('INTELLIGENT','NOOB')
,ROW('ZXC',NULL)
,ROW('CXX','1')];
BEGIN
CALL stored_example(parameters);
END
$$
LANGUAGE plpgsql
C# Npgsql (nuget Npgsql 4.1.4)
// mapping global type on Startup.cs
NpgsqlConnection.GlobalTypeMapper.MapComposite<UdtSpParameter>("udt_sp_parameter");
// class model UdtSpParameter
public class UdtSpParameter
{
[PgName("field")]
public string Field { get; set; }
[PgName("value")]
public string Value { get; set; }
public UdtSpParameter() { }
}
// call stored procedure at data access layer for example StudentDAL.cs
public IEnumerable<T> CallStoredResultSet<T>(UdtSpParameter[] inputParameters ) where T : class
{
var conn = _GetOpenConnection();
var tran = _BeginTransaction(conn);
NpgsqlCommand command = new NpgsqlCommand("stored_example", conn);
command.CommandType = CommandType.StoredProcedure;
var cmdParam = command.CreateParameter();
cmdParam.ParameterName = "parameters";
cmdParam.DbType = DbType.Object;
cmdParam.Value = inputParameters;
cmdParam.DataTypeName = "udt_sp_parameter";
command.Parameters.Add(cmdParam);
// throw exception here
// Can't write CLR type Web.API.Models.UdtSpParameter[] with handler type MappedCompositeHandler`1
NpgsqlDataReader dr = command.ExecuteReader();
var result = new List<T>();
while (dr.Read())
{
Console.WriteLine(dr[0].ToString(), dr[1].ToString());
}
_CommitTransaction(tran);
_CloseConnection(conn);
return result;
}
Please find some stuff if I do anything wrong and point me to fix that. Thanks in advance.
You have this error because you have specified the type name for the parameter which is the composite type and not an array of composites. Therefore, you should specify udt_sp_parameter[] as the value of DataTypeName instead of udt_sp_parameter:
var cmdParam = command.CreateParameter();
cmdParam.ParameterName = "parameters";
cmdParam.Value = inputParameters;
cmdParam.DataTypeName = "udt_sp_parameter[]";
Since your type is registered and the driver already knows its PostgreSQL name, there is no need to specify it during the parameter setup. Feel free to remove the last line from the code above:
var cmdParam = command.CreateParameter();
cmdParam.ParameterName = "parameters";
cmdParam.Value = inputParameters;
The driver is smart enough to detect types automatically in most cases, but when there is an ambiguity then it should be specified. For example, you have a string which should be passed as JSON, or a CLR type should be serialized to JSON. In other cases just rely on the driver internals and allow him do his work.
The official documentation of Npgsql says:
The only way to call a stored procedure is to write your own CALL
my_proc(...) command, without setting CommandBehavior.StoredProcedure.
In your particular case you should modify your code like this:
NpgsqlCommand command = new NpgsqlCommand("call stored_example(:parameters)", conn);
// comment this line command.CommandType = CommandType.StoredProcedure;
Hope it helps bro.

Troubleshooting "Data type mismatch in criteria expression." during MS Access Insert.Into

I'm creating a basic customer inventory application, and when converting the code from using SQL Server to using MS Access (which I'm quite a bit less versed in), I ran into a "Data type mismatch" error when trying to do a basic insert.
I've looked into several similar questions here, and double checked the msdn syntax guide, but I can't find a reason why the script I've written would generate that error. I changed my code several times to try and ensure proper data type (ending up with what I have below with explicit typing and adding the value later). I've actually even taken the string and pasted it into MS Access (sans white space and double quotes), and it seems to work just fine with the values given. At this point, I'm really and truly stumped, and I'm wondering if it might just be a quirk with the Oledb adapter? Any help would be appreciated. Thanks.
// SQL query defined elsewhere:
public static readonly string sqlAddCustomerNotes = "INSERT INTO CustomerNotes (Customer_ID, Notes, NotesDate) "
+ "VALUES(#Customer_ID, #Notes, #NotesDate);";
// end sql query
// data access function
public static void addNotes(int customerID, string notes, DateTime notesDate)
{
string query = Scripts.sqlAddCustomerNotes;
using (
OleDbCommand dbCommand = new OleDbCommand()
{
Connection = new OleDbConnection(ConnectionAccess.connString),
CommandType = CommandType.Text,
CommandText = query,
Parameters =
{
new OleDbParameter("#Customer_ID", OleDbType.Integer),
new OleDbParameter("#Notes", OleDbType.LongVarChar),
new OleDbParameter("#NotesDate", OleDbType.DBTimeStamp)
}
}) // end using parenthetical
{ // begin using scope
dbCommand.Parameters[0].Value = customerID;
dbCommand.Parameters[1].Value = notes;
dbCommand.Parameters[2].Value = notesDate;
foreach (OleDbParameter param in dbCommand.Parameters)
{ // replace ambiguous null values with explicit DBNulls.
if (param.Value == null)
{
param.Value = DBNull.Value;
}
}
dbCommand.Connection.Open();
int rowsAffected = dbCommand.ExecuteNonQuery();
dbCommand.Connection.Close();
Console.WriteLine($"Rows affected: {rowsAffected}");
}
} // end addCustomerNotes
/*
table "CustomerNotes" has the following columns:datatypes
CustomerNotes_ID: AutoNumber
Customer_ID: Number
Notes: Memo
NotesDate: Date/Time
CreateDate: Date/Time
test case (in code) was:
#Customer_ID = 5
#Notes = "customer might change last name to simpson."
#NotesDate = {6/26/2019 12:05:39 PM}
*/
It probably is a date, not a timestamp:
new OleDbParameter("#NotesDate", OleDbType.DBDate)
Considering June7's comment about delimiters, it seems the issue lies in some issue inherent to the OleDbParameter type. In SQL Server terms, I do want DateTime (not Date), but representing it as a DBTimeStamp seems to make it unrecognizable by Access.
For the time being, I've sent the date as a VarChar and allowed Access to convert it however its internal engine sees fit. It feels/seems wrong, but it does, in fact, solve the problem.
Parameters =
{
new OleDbParameter("#Customer_ID", OleDbType.Integer),
new OleDbParameter("#Notes", OleDbType.LongVarChar),
new OleDbParameter("#NotesDate", OleDbType.VarChar)
}
EDIT: Just saw June7's latest comment, and there was in fact, an answer in another thread. OleDbType.DBDate doesn't do what I want, but OleDbType.Date does.

Loop through request.form and add data with db.Execute [duplicate]

Using Database.Query has has made a huge improvement on readability in my code:
String Select = 'sp_getAllUsers';
WebMatrix.Data.Database DB = WebMatrix.Data.Database.Open(MyConString);
var data = DB.Query(Select);
I'd like to switch to a non-stored procedure query. The MSDN says there's an optional parameter to the Query Method, Object[], that can be passed as SQL parameters, however they don't have any further information about it.
So I have two questions:
How can I create a Object[]?
Will adding parameters in this way prevent hacking threats, such as SQL Injection?
Here's an abridged version of what I have tried:
Select = "Select * From Users Where first_name = "Foo" AND last_name = "Bar"
// Like Javascript
Object[] params = {"first_name" : "Foo"}, {"last_name" : "Bar"};
// More Like What I think it will be
Object[] Params = (String Name = "first_name", String First_Name = "Foo");
var data = DB.Query(Select, params);
All the sources I've looked at only seem to reference the old way. This is close, but he doesn't use the parameter parameter of the Query method.
Try using this syntax:
string selectCommand = "sp_getAllUsers(#0, #1)";
// selectCommand = "Select * From Users Where first_name = #0 AND last_name = #1";
...
var data = DB.Query(selectCommand, "Foo", "Bar");
More info, see:
http://www.aspnet101.com/2010/07/webmatrix-tutorial-working-with-data/
Also, using a Parameter will always prevent SQL Injection as it always double quote a single quote.

How to translate table defintion from MS Access to Oracle using C# and OleDbConnection?

I need to copy a table from MS Access to Oracle without using OracleBulkCopy (using Managed Data Access).
First step is to create the table.
As can be seen in the code below I am now converting the fields manually while querying for the column length.
I would like to use a statement that converts the fields for me without having to add rules for all types manually since there are a lot of types.
Or better, just extract some sort of DDL that I can execute in Oracle.
Is this possible?
private int GetLength(string accessTableName, string columnName, OleDbConnection accessConnection)
{
columnName = $"[{columnName}]";
var accessCommand = new OleDbCommand($"select max(len({columnName})) from {accessTableName}", accessConnection);
return int.Parse(accessCommand.ExecuteScalar().ToString());
}
private void CreateEmptyTable(DataTable schemaTable, string tableName, OracleConnection oracleConnection, string accessTableName, OleDbConnection accessConnection)
{
var columnSpecs = new string[schemaTable.Rows.Count];
for (int i = 0; i < schemaTable.Rows.Count; ++i)
{
var name = schemaTable.Rows[i].ItemArray[0].ToString();
var dataType = schemaTable.Rows[i].ItemArray[5];
//var length = schemaTable.Rows[i].ItemArray[2];
var length = GetLength(accessTableName, name.ToString(), accessConnection);
var precision = schemaTable.Rows[i].ItemArray[3];
var scale = schemaTable.Rows[i].ItemArray[4];
var oracleDt = "";
switch (dataType.ToString())
{
case "System.String":
oracleDt = $"nchar({length})";
break;
case "System.Int32":
case "System.Int16":
var iLng = int.Parse(length.ToString()) * 2;
oracleDt = $"number({iLng},0)";
break;
case "System.Double":
case "System.Decimal":
oracleDt = $"number({length},{precision})";
break;
default:
throw new Exception();
}
name = name.ToString().ToUpper().Replace(' ', '_');
columnSpecs[i] = $"{name} {oracleDt}";
}
var query = $"create table MDB_{tableName.ToUpper().Replace(' ', '_')} ( {string.Join(",", columnSpecs)} )";
var oracleCommand = new OracleCommand(query, oracleConnection);
oracleCommand.ExecuteNonQuery();
}
Perhaps you try executing the command on the base table in place of the query.
So try something like this:
DoCmd.TransferDatabase acExport, "ODBC Database", strCon,
acTable, "tblHotels2", "tblHotelEXPORT", True
Now above is from Access, but your posted answer is on the right track.
When I send above to SQL server, long, money, memo, text files all get sent out and created on sql server. I suppose you could use above, and then execute a series of alter table to change the types, but that would be painful.
So before the pain approach, I would try above. Note that:
I used the transfer command on the base table, not a query.
I used "True" as that last value - that means no data copy, just structure.
You could also try/test a different ORACLE odbc driver. I don't have a test oracle server around, but sending the above to SQL server - it did a VERY nice job of creating the data types on the server side.
This option uses Interop instead but the result is disappointing. Numeric fields are converted to VARCHAR2, haven't tested other types yet. Note that it also requires a DSN file, and it requires the Oracle ODBC drivers.
For the people who could use this, here is the code:
public void CreateTableDefsUsingInterop(string accessFilePath, string accessTable, string oracleUser, string oraclePassword, string oracleTable, string oracleDSNFilePath)
{
var strConn = $"ODBC;FILEDSN={oracleDSNFilePath};UID={oracleUser};PWD={oraclePassword}";
var sTypExprt = "ODBC Database";
var interop = new Application();
interop.OpenCurrentDatabase(accessFilePath);
var db = interop.CurrentDb();
var emptyTable = $"n{accessTable}";
QueryDef qd;
qd = db.CreateQueryDef("access2ora1", $"select top 1 * into [{emptyTable}] from [{accessTable}]");
qd.Execute();
qd = db.CreateQueryDef("access2ora2", $"delete from [{emptyTable}]");
qd.Execute();
interop.DoCmd.TransferDatabase(AcDataTransferType.acExport, sTypExprt, strConn, AcObjectType.acTable, emptyTable, oracleTable);
interop.Quit(AcQuitOption.acQuitSaveNone);
}

Executing stored procedure using linq in c#

I made a stored procedure in sql server 2008 which gives me the changes made to a table. I am using Linq to SQL to use this table in C#.
my stored procedure is
CREATE PROCEDURE dbo.getlog
-- Add the parameters for the stored procedure here
#p1 int = 0,
#p2 int = 0
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
-- Insert statements for procedure here
DECLARE #from_lsn binary(10), #to_lsn binary(10)
SET #from_lsn =
sys.fn_cdc_get_min_lsn('dbo_User_Info')
SET #to_lsn = sys.fn_cdc_get_max_lsn()
SELECT ID_number, Name, Age FROM cdc.fn_cdc_get_all_changes_dbo_User_Info
(#from_lsn, #to_lsn, N'all');
END
GO
The above procedure runs fine in sql server. However when i run this statement using linq in C#
mytestDataContext obj = new mytestDataContext();
var test=obj.ExecuteCommand("dbo.getlog");
foreach( var abc in test)
{}
I get this error
Error 1 foreach statement cannot operate on variables of type 'int'
because 'int' does not contain a public definition for
'GetEnumerator'
ExecuteCommand returns an int.. not your results.
See MSDN here: http://msdn.microsoft.com/en-us/library/system.data.linq.datacontext.executecommand.aspx
public int ExecuteCommand(
string command,
params Object[] parameters
)
I think you're after ExecuteQuery.
ExecuteCommand method returns Int32 and you can't use magical foreach loop using a simple integer.
Return Value
Type: System.Int32
The number of rows modified by the executed command.
I'm not too much familiar with DataContext class but you can use DataContext.ExecuteQuery which returns IEnumerable<TResult> and you can use foreach loop with it.
Return Value
Type: System.Collections.Generic.IEnumerable<TResult>
A collection of objects returned by the query.
I don't know why do you use foreach-statement, but method 'ExecuteCommand' returns int value, and foreach cycle need object that implements IEnumerable
I may be assuming too much, but if you are doing the C# with a recent version of Visual Studio, rather directly specifying the T-SQL call to run the stored procedure as string of literal text, you can drag and drop the stored procedure onto the LINQ to SQL modelling window. This will add it to the LINQ to SQL data context.
int param1 = 1;
int param2 = 2;
mytestDataContext obj = new mytestDataContext();
var test=obj.getlog(param1, param2);
foreach( var abc in test)
{
/* code inside loop */
}
The same technique can be used for calling user-defined functions.
Doing this will reduce typing and provide intellisense to help with calling the stored procedures and SQL functions.
You can use this library:
https://github.com/mrmmins/C-StoreProcedureModelBinding
Returns the values as a List, you only need create a simple class with the names and values types, like:
var productos = DataReaderT.ReadStoredProceadures<MyCustomModel>(myDbEntityInstance, "dbo.MySPName", _generic);
and MyCumtomModel class is something like:
public int id {get; set;}
public int salary {get; set;}
public string name {get; set;}
public string school {get; set;}
and generic like:
List<Generic> _generic = = new List<Generic>
{
new Generic
{
Key = "#phase", Type = SqlDbType.Int, Value = "207"
}
}
};
And now, your products has the options like: products.First(), products.Count(), foreach etc.

Categories