3

I am trying to pass my DataTable to a stored procedure. The DataTable holds the contents of an Excel sheet. The Excel sheet has two empty columns, five with text, and five with decimal numbers. The empty columns are OppdragsMVAkode and OppdragsgebyrMVAkode.

If I run my application, I get this exception:

System.ArgumentException: Input string was not in a correct format. Couldn't store <> in OppdragsMVAkode Column. Expected type is Decimal.

If I add a temporary number, I get this exception:

System.Data.SqlClient.SqlException: Error converting data type nvarchar to numeric.

I don't get why it's reading it as a string.

My stored procedure (spGetTrips):

ALTER PROCEDURE [dbo].[spGetTrips]
    @trips udtTripsPerMonth readonly
    
AS
BEGIN
    INSERT INTO tblTripsPerMonth
        SELECT 
        [KjøretøyID], 
        SUBSTRING([År], 7, 4), 
        SUBSTRING([Måned], 4, 2), 
        [Betaling (brutto)], 
        [Betaling (netto)], 
        [Bestillingsgebyr (netto)], 
        [Betalingsgebyr (netto)], 
        [OppdragsMVAkode], 
        CONCAT(LøyvehaverFakturaID, + 'UF-' + SUBSTRING([År], 9, 2) + SUBSTRING([Måned], 4, 2)), 
        [Oppdragsgebyr (netto)], 
        [OppdragsgebyrMVAkode], 
        CONCAT([RidelRegionFakturaID], + 'UF-' + SUBSTRING([År], 9, 2) + SUBSTRING([Måned], 4, 2))
    FROM @trips

    UPDATE tblTripsPerMonth
    SET [OppdragsMVAkode] = (
        SELECT [ID]
        FROM [tblMVAkoder]
        WHERE [ID] = 'MVAkode2'
        );

    UPDATE tblTripsPerMonth
    SET [OppdragsgebyrMVAkode] = (
        SELECT [ID]
        FROM [tblMVAkoder]
        WHERE [ID] = 'MVAkode5'
        );
END

As you can see above, I am setting the value of the two empty columns with UPDATE clauses in the Stored Procedure. Whether they are empty in the Excel sheet, or with some preliminary value, and then to be overridden, I care not - I just want it to work.

Here's my User-Defined Table Type (udtTripsPerMonth):

CREATE TYPE [dbo].[udtTripsPerMonth] AS TABLE(
    [KjøretøyID] [nvarchar](50) NULL,
    [År] [nvarchar](50) NULL,
    [Måned] [nvarchar](50) NULL,
    [Betaling (brutto)] [decimal](10, 2) NULL,
    [Betaling (netto)] [decimal](10, 2) NULL,
    [Bestillingsgebyr (netto)] [decimal](10, 2) NULL,
    [Betalingsgebyr (netto)] [decimal](10, 2) NULL,
    [OppdragsMVAkode] [decimal](10, 2) NULL,
    [LøyvehaverFakturaID] [nvarchar](50) NULL,
    [Oppdragsgebyr (netto)] [decimal](10, 2) NULL,
    [OppdragsgebyrMVAkode] [decimal](10, 2) NULL,
    [RidelRegionFakturaID] [nvarchar](50) NULL
)
GO

And my table (tblTripsPerMonth):

CREATE TABLE [dbo].[tblTripsPerMonth](
    [ID] [int] IDENTITY(1,1) NOT NULL,
    [KjøretøyID] [nvarchar](50) NULL,
    [År] [nvarchar](50) NULL,
    [Måned] [nvarchar](50) NULL,
    [Betaling (brutto)] [decimal](10, 2) NULL,
    [Betaling (netto)] [decimal](10, 2) NULL,
    [Bestillingsgebyr (netto)] [decimal](10, 2) NULL,
    [Betalingsgebyr (netto)] [decimal](10, 2) NULL,
    [OppdragsMVAkode] [decimal](10, 2) NULL,
    [LøyvehaverFakturaID] [nvarchar](50) NULL,
    [Oppdragsgebyr (netto)] [decimal](10, 2) NULL,
    [OppdragsgebyrMVAkode] [decimal](10, 2) NULL,
    [RidelRegionFakturaID] [nvarchar](50) NULL,
PRIMARY KEY CLUSTERED 
(
    [ID] ASC
)WITH (STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO

The other table I reference in the UPDATE clauses (tblMVAkoder):

CREATE TABLE [dbo].[tblMVAkoder](
    [ID] [nvarchar](50) NOT NULL,
    [Startdato] [date] NULL,
    [Sluttdato] [date] NULL,
    [MVAsats] [decimal](10, 2) NULL,
PRIMARY KEY CLUSTERED 
(
    [ID] ASC
)WITH (STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO

Here's my C# code (Excel to DataTable):

private void btnExport_Click(object sender, RoutedEventArgs e) {

    OpenFileDialog of = new();
    of.Filter = "Excel Files | *.xlsx;";
    of.Title = "Importer Excel fil.";

    if (of.ShowDialog() == true) {

        dgPaidTrip.ItemsSource = ImportExceltoDataTable(of.FileName).DefaultView;
        btnClearForm.IsEnabled = true;
    }

    using (SqlConnection con = new(ConnectionString.connectionString))
    using (var cmd = new SqlCommand("spGetTrips", con) { CommandType = CommandType.StoredProcedure }) {

        con.Open();
        DataTable dt = ImportExceltoDataTable(of.FileName);
        cmd.Parameters.Add(new SqlParameter("@trips", dt));
        cmd.ExecuteNonQuery();
    }
}

public static DataTable ImportExceltoDataTable(string filePath) {

    using (XLWorkbook wb = new(filePath)) {

        IXLWorksheet ws = wb.Worksheet(1);
        int tl_Row = ws.FirstCellUsed().Address.RowNumber;
        int tl_Col = ws.FirstCellUsed().Address.ColumnNumber;
        int br_Row = ws.LastCellUsed().Address.RowNumber;
        int br_Col = ws.LastCellUsed().Address.ColumnNumber;

        DataTable dt = new();

        dt.Columns.Add("KjøretøyID", typeof(string));
        dt.Columns.Add("År", typeof(string));
        dt.Columns.Add("Måned", typeof(string));
        dt.Columns.Add("Betaling (brutto)", typeof(decimal));
        dt.Columns.Add("Betaling (netto)", typeof(decimal));
        dt.Columns.Add("Bestillingsgebyr (netto)", typeof(decimal));
        dt.Columns.Add("Betalingsgebyr (netto)", typeof(decimal));
        dt.Columns.Add("OppdragsMVAkode", typeof(decimal));
        dt.Columns.Add("LøyvehaverFakturaID", typeof(string));
        dt.Columns.Add("Oppdragsgebyr (netto)", typeof(decimal));
        dt.Columns.Add("OppdragsgebyrMVAkode", typeof(decimal));
        dt.Columns.Add("RidelRegionFakturaID", typeof(string));

        IXLRow currentRow;
    
        for (int dtRow = 0; dtRow < br_Row - tl_Row; dtRow++) {

            currentRow = ws.Row(tl_Row + dtRow + 1);
            dt.Rows.Add();

            for (int dtCol = 0; dtCol < br_Col - tl_Col + 1; dtCol++) {

                dt.Rows[dtRow][dtCol] = currentRow.Cell(tl_Col + dtCol).Value;
            }
        }

        return dt;
    }
}

As far as I can see, all my column types match, as well as their order. I changed all my types to nvarchar, and the code "works". But someone smarter than me told me it was not a good idea to fake the column types.

What am I missing?

Ole M
  • 317
  • 1
  • 17
  • 2
    Have you tried setting the `SqlDBType` to `Structured` for your `DataTable` you are sending in? The first error is related to `ImportExceltoDataTable` => `dt.Rows[dtRow][dtCol] = currentRow.Cell(tl_Col + dtCol).Value;` FWIW I would be validating that data first *before* trying to put it in the table/columns. – Trevor Sep 15 '21 at 19:39
  • 1
    For the second issue, I would put some kind of try/catch/begin catch statement in your procedures to help track down exactly where the issue is occurring. You can get the procedure name, the line number, the exact message etc. and log that error in another table or return it and check. – Trevor Sep 15 '21 at 19:49
  • 2
    As pointed out the problem is with `dt.Rows[dtRow][dtCol] = currentRow.Cell(tl_Col + dtCol).Value`, which is because it's adding all cell values as string data. If the Cell.Values for `Betaling (brutto)`, `Betaling (netto)`, `Bestillingsgebyr (netto)`, `Betalingsgebyr (netto)`, `OppdragsMVAkode`, `Oppdragsgebyr (netto)` and `OppdragsgebyrMVAkode` are blank, or don't look like decimal numbers, you should probably send `DBNull.Value` instead of passing the string along verbatim. – AlwaysLearning Sep 15 '21 at 21:52
  • @zaggler thanks for your comment. I did try to set the `SqlDBType` to `Structured`, but the exceptions persist. I have been trying to change the line you and @AlwaysLearning (thanks for your comment as well) is referencing, but I haven't been successful. How would I send DBNull.Value? I tried adding `AllowDBNull = true` to the columns in question, in my `dt.Columns.Add` section, but that did not work. – Ole M Sep 16 '21 at 10:01

1 Answers1

3

To pass a null value, you need to set the column value as DBNull.Value. You can set DBNull like this

dt.Rows[dtRow][dtCol] = currentRow.Cell(tl_Col + dtCol).Value ?? (object)DBNull.Value;

You must also set SqlDBType TypeName and Direction properties

cmd.Parameters.Add(
    new SqlParameter("@trips", SqlDBType.Structured)
    {
        TypeName = "dbo.udtTripsPerMonth",
        Direction = ParameterDirection.Input,
        Value = dt
    });
Charlieface
  • 52,284
  • 6
  • 19
  • 43
  • thank you for this! This was clarifying. After implementing this, I now will not get an exception if I add a temporary numeric value to the columns. That is okay, since I can use the `UPDATE` clause to edit them. But I would like to understand why it still gives me the "Input string was not in a correct format. Couldn't store <> in OppdragsMVAkode Column. Expected type is Decimal." exception if I keep the column empty. Any ideas? – Ole M Sep 17 '21 at 07:51
  • If the value is `<>` I don't see how that can be converted to a `decimal`. You would need to check for that value yourself and null it out. – Charlieface Sep 17 '21 at 09:11
  • < > just means it's an empty string, right? There's no content in the cells in that particular column. – Ole M Sep 17 '21 at 09:50
  • 1
    No idea. i can't see the value you are seeing. Either it's an empty string (`== ""` is true) or it's not (`== "<>"` is true) you need to work that out yourself. An empty string is not the same as null, and is also not convertible to `decimal`, so you need to check for that – Charlieface Sep 17 '21 at 10:00
  • thanks for all the help! – Ole M Sep 17 '21 at 12:31