0

I have an API with a Post endpoint where the body is quite flexible. Basically I allow anything to be posted there and then I will serialize this body to an object, like this:

  public class PostBody
    {
        public string Id{ get; set; }
        public string SomeOtherValue{ get; set; }
        public string AnotherValue{ get; set; }
    }


    string body = 
   "{
            "id": "335",
            "someOtherValue": "fsdfds"
    }"

PostBody body = JsonConvert.DeserializeObject<PostBody>(body);

As you can see here Postbody can be populated with "AnotherValue" but it also might not depending on what the client wants to insert/update.

And lets say that my database table contains all three, "Id", "SomeOtherValue" and "AnotherValue" as columns.

Now, how can I use dapper to dynamically insert/update this table, depending if the properties in the PostBody object are null or not? I mean if "AnotherValue" is null, the query should only be like this:

INSERT INTO table
(
[Id], [SomeOtherValue]
)
VALUES
( 
@Id, @SomeOtherValue
)

But if "AnotherValue" is not null, the query should be like this:

INSERT INTO table
    (
    [Id], [SomeOtherValue], [AnotherValue]
    )
    VALUES
    ( 
    @Id, @SomeOtherValue, @AnotherValue
    )

Is there a nice way to do this automatically like with a mapper or similar? Or else I would have to code a very long if statement, concatenating a query together. It would me messy and difficult to maintain.

Edit:

This is how my insert looks today:

using System;
using System.Data.SqlClient;
using System.Threading.Tasks;

namespace Api.Actions.Commands
{
    class InsertStuffRequest
    {
        public InsertStuffRequest( string id, string SomeOtherValue, string AnotherValue)
        {
            Id=id;
            SomeOtherValue = someOtherValue;
            AnotherValue = ;notherValue;
        }


        public string Id { get; }
        
        public string SomeOtherValue { get; }
        
        public string AnotherValue { get; }

        public interface IInsertStuffCommand : IQuery<InsertStuffRequest, bool>
        {
        }

        public class InsertStuffCommand : IInsertStuffCommand
        {
            private const string Command =
                @"INSERT INTO table
    (
    [Id], [SomeOtherValue], [AnotherValue]
    )
    VALUES
    ( 
    @Id, @SomeOtherValue, @AnotherValue
    )";

            public async Task<Result<bool>> ExecuteAsync(InsertStuffRequest request,
                IApplicationContext context)
            {
                using (SqlConnection connection = new SqlConnection(context.StuffConnectionString))
                {
                    await connection.OpenAsync();

                    var rowsAffected = await connection
                        .ExecuteAsyncWithRetry
                        (
                            Command, request
                        );

                    return Result.From(rowsAffected > 0);
                }
            }
        }
    }
}

This will of course work fine for inserts but its more difficult for updates. Also my Post endpoint will work with different tables that have different columns.

dfsdf
  • 1
  • 1
  • I have added it now. – dfsdf Sep 30 '20 at 09:34
  • @dfsdf I suspect you need [Dapper.Contrib](https://github.com/StackExchange/Dapper/tree/main/Dapper.Contrib). It provides simple, convention-based CRUD operations so you don't have to write the statements by hand – Panagiotis Kanavos Sep 30 '20 at 10:35
  • BTW `InsertStuffRequest` breaks the design of Dapper. Instead of a simple DTO, this is a Data Access Object with CRUD operations, exposing a single object's fields. Dapper is used as little more than an alternative ADO.NET syntax. It's *Dapper's* job to perform all the CRUD operations though. In fact, I suspect Dapper contains some similar code to create and execute DbCommand instances – Panagiotis Kanavos Sep 30 '20 at 10:38
  • Dapper also supports [custom type and property mapping](https://stackoverflow.com/a/12615036/134204). – Panagiotis Kanavos Sep 30 '20 at 10:58

2 Answers2

1

If your classes match your tables you could use Dapper.Contrib. The library will create the appropriate INSERT or UPDATE queries based on the objects passed to it.

From the examples, for this class :

public class Car
{
    public int Id { get; set; } // Works by convention
    public string Name { get; set; }
}

You can insert new rows with :

connection.Insert(new Car { Name = "Volvo" });

or insert a list with

connection.Insert(cars);

You can update the same way:

connection.Update(new Car() { Id = 1, Name = "Saab" });
Panagiotis Kanavos
  • 120,703
  • 13
  • 188
  • 236
  • Thanks! Sadly the names in the database are garbage so I have to use proper names in the API and then map them to the bad names in the db. – dfsdf Sep 30 '20 at 10:51
  • You didn't mention that in the question. You can use Dapper's mapping attributes or [custom type mapping](https://stackoverflow.com/a/12615036/134204). You'll have to explain what you want in the question itself so people can post code the appropriate mapping code – Panagiotis Kanavos Sep 30 '20 at 10:58
  • I have to hand it to you. It's a great solution. Thanks for introducing Dapper.Contrib. @PanagiotisKanavos – Ghanat Sep 30 '20 at 12:42
0

I think your question is just a mistake Use your second insert:

INSERT INTO table
    (
    [Id], [SomeOtherValue], [AnotherValue]
    )
    VALUES
    ( 
    @Id, @SomeOtherValue, @AnotherValue
    )

and pass a null value (or empty string) to @AnotherValue when you haven't got value.

For the update query, you must generate a dynamic string by sqlbuilder: (this code not tested and wrote as sample only)

SqlBuilder builder = new SqlBuilder();
    var update = builder.AddTemplate("UPDATE table /**set**/ OUTPUT inserted.id /**where**/");    
    builder.Set("SomeOtherValue= SomeOtherValue", new { StrSomeOtherValue});
    if (!string.IsNullOrEmpty(StrAnotherValue))
    {
        builder.Set("AnotherValue= @AnotherValue", new { StrAnotherValue });
    }
    
    builder.Where("id = @id", new { id = entityId});
    
    var result = new Result<long?>(sqlConn.ExecuteScalar<long?>(update.RawSql, update.Parameters));
Ghanat
  • 158
  • 1
  • 11
  • Yeah, this would work for inserts. But my API also supports Updates. And sometimes maybe you only provide a body for one field, then all the other fields would be set to null, thus overriding previous inserted data. – dfsdf Sep 30 '20 at 09:28
  • I think I found the solution: https://stackoverflow.com/questions/2675968/sql-how-can-i-update-a-value-on-a-column-only-if-that-value-is-null ISNULL will return the first value if it is non null, otherwise it will use the column value – dfsdf Sep 30 '20 at 09:44
  • @dfsdf What a nice solution you have found at https://stackoverflow.com/questions/2675968/sql-how-can-i-update-a-value-on-a-column-only-if-that-value-is-null. I think so COALESCE() can be useful but in many complex cases SqlBuilder can help more – Ghanat Sep 30 '20 at 10:04
  • I have not tried isnull yet. Thanks for showing SqlBuilder, I might have use for it! – dfsdf Sep 30 '20 at 10:21
  • Hmm, antoher thing I was thinking of. What if the client actually wants to set the value to null, I wonder how to deal with it then, – dfsdf Sep 30 '20 at 11:21
  • @dsdf I think so it is related to your business. If your field is the mandatory use something like this: builder.Set("SomeOtherValue= SomeOtherValue", new { StrSomeOtherValue}); otherwise use if (!string.IsNullOrEmpty(StrAnotherValue)). Finally, I think the best thing to do is flowing PanagiotisKanavos 's solution. It is clear and supports all of these problems. – Ghanat Sep 30 '20 at 12:30