Search code examples
c#sql-serverasp.net-coredapper

Unable to use the Dapper Multimap when objects to map share a type (SQL restriction of having unique column names)


I am trying to bind my dapper queries to some models I have laid out. 4 of my objects use a User model as a type with and fields INTERNAL_USER_NUMBER, FIRST_NAME, LAST_NAME etc. However in SQL Server, you cannot have a query with identical columns.

So in my Dapper multimap function I am not able to split on INTERNAL_USER_NUMBER 4 times to bind my data. Not sure how to solve this:

var certificationReviews2 = await connection.QueryAsync<ARMS_API.Models.CertificationReview>("select * from dbo.A2F_DMCP_0005_BrowseTest()", 
types: new[] { 
    typeof(ARMS_API.Models.CertificationReview), 
    typeof(State), 
    typeof(ARMS_API.Models.Program), 
    typeof(ActuarialFirm), 
    typeof(User), 
    typeof(User), 
    typeof(User) , 
    typeof(User)
    },
map: (objects) => {
    var certificationReview = (ARMS_API.Models.CertificationReview)objects[0];
    certificationReview.STATE = (State)objects[1];
    certificationReview.STATE.PROGRAMS.Add((ARMS_API.Models.Program)objects[2]);
    certificationReview.ACTUARIAL_FIRM = (ActuarialFirm)objects[3];
    certificationReview.ACTUARIAL_FIRM.CERTIFYING_ACTUARY.Add((User)objects[4]);
    certificationReview.PRIMARY_REVIEWER = (User)objects[5];
    certificationReview.SECONDARY_REVIEWER = (User)objects[6];
    certificationReview.DMCP_ANALYST = (User)objects[7];

    return certificationReview;
},
splitOn: "STATE_ID,PROGRAM_DD,ACTUARIAL_FIRM_DD,INTERNAL_USER_NUMBER,INTERNAL_USER_NUMBER,INTERNAL_USER_NUMBER,INTERNAL_USER_NUMBER");

Updated coding (4/2/24) :

                var certificationReviews = await connection.QueryAsync<ARMS_API.Models.CertificationReview>("select * from dbo.A2F_DMCP_0005_BrowseTest()", 
                types: new[] { 
                    typeof(ARMS_API.Models.CertificationReview), 
                    typeof(State), 
                    typeof(ARMS_API.Models.Program), 
                    typeof(ActuarialFirm), 
                    typeof(object), 
                    typeof(object), 
                    typeof(object) , 
                    typeof(object)
                    },
                map: (objects) => {
                    var certificationReview = (ARMS_API.Models.CertificationReview)objects[0];
                    certificationReview.STATE = (State)objects[1];
                    certificationReview.STATE.PROGRAMS.Add((ARMS_API.Models.Program)objects[2]);
                    certificationReview.ACTUARIAL_FIRM = (ActuarialFirm)objects[3];
                    certificationReview.ACTUARIAL_FIRM.CERTIFYING_ACTUARY.Add(new User {
                        INTERNAL_USER_NUMBER = (int?)(object)objects[4].GetType().GetProperty("CERTIFYING_ACTUARY_ID"),
                        FIRST_NAME = (string?)(Object)objects[4].GetType().GetProperty("CERTIFYING_ACTUARY_FIRST_NAME"),
                        LAST_NAME = (string?)(Object)objects[4].GetType().GetProperty("CERTIFYING_ACTUARY_LAST_NAME")});
                    certificationReview.PRIMARY_REVIEWER = new User {
                        INTERNAL_USER_NUMBER = (int?)(object)objects[4].GetType().GetProperty("PRIMARY_ACTUARY_ID"),
                        FIRST_NAME = (string?)(Object)objects[4].GetType().GetProperty("PRIMARY_ACTUARY_FIRST_NAME"),
                        LAST_NAME = (string?)(Object)objects[4].GetType().GetProperty("PRIMARY_ACTUARY_LAST_NAME")};
                    certificationReview.SECONDARY_REVIEWER = new User {
                        INTERNAL_USER_NUMBER = (int?)(object)objects[4].GetType().GetProperty("SECONDARY_ACTUARY_ID"),
                        FIRST_NAME = (string?)(Object)objects[4].GetType().GetProperty("SECONDARY_ACTUARY_FIRST_NAME"),
                        LAST_NAME = (string?)(Object)objects[4].GetType().GetProperty("SECONDARY_ACTUARY_LAST_NAME")};
                    certificationReview.DMCP_ANALYST = new User {
                        INTERNAL_USER_NUMBER = (int?)(object)objects[4].GetType().GetProperty("DMCP_ANALYST_ID"),
                        FIRST_NAME = (string?)(Object)objects[4].GetType().GetProperty("DMCP_ANALYST_FIRST_NAME"),
                        LAST_NAME = (string?)(Object)objects[4].GetType().GetProperty("DMCP_ANALYST_LAST_NAME")};

                    return certificationReview;
                },
                splitOn: "STATE_ID,PROGRAM_DD,ACTUARIAL_FIRM_DD,CERTIFYING_ACTUARY_ID,PRIMARY_ACTUARY_ID,SECONDARY_ACTUARY_ID,DMCP_ANALYST_ID");
            return (IEnumerable<CertificationReview>)certificationReviews;
        }        
    }

This still is not binding the user data. I have the spliton set correctly. I manually map but use object instead of dynamic and type cast it to the specified value. Still null/not mapping.


Solution

  • You're facing a problem with Dapper's QueryAsync method when trying to bind data from a SQL Server query to your models. Your query involves multiple instances of a User model, SQL Server does not allow for queries to return multiple columns with the same name, so you cannot use the same splitOn parameter (e.g., INTERNAL_USER_NUMBER) multiple times for different User objects.

    To resolve this issue you could use SQL column aliases to ensure that each instance of similar data (e.g., each User model's INTERNAL_USER_NUMBER, FIRST_NAME, LAST_NAME, etc.) has a unique name in the result set. This involves changing your SQL query so that each field related to different User objects has a distinct alias. For example, prefixing or suffixing the column names with something indicative of their role or position (e.g., CertifyingActuary_INTERNAL_USER_NUMBER, PrimaryReviewer_INTERNAL_USER_NUMBER, etc.).In your Dapper QueryAsync call, use the splitOn parameter to specify the unique alias for the first column of each set of data that you want to map to a different object. This tells Dapper exactly where to split the data for mapping to your models. The splitOn parameter should match the first aliased column name for each object set coming from your modified SQL query.

    sql query:

    SELECT 
        cr.*, -- this fetches CertificationReview fields
        s.STATE_ID, s.OtherStateFields, -- State fields with aliases if needed
        p.PROGRAM_DD, p.OtherProgramFields, -- Program fields with aliases if needed
        af.ACTUARIAL_FIRM_DD, af.OtherActuarialFirmFields, -- ActuarialFirm fields
        ca.INTERNAL_USER_NUMBER as CertifyingActuary_USER_NUMBER, ca.FIRST_NAME as CertifyingActuary_FIRST_NAME, -- etc
        pr.INTERNAL_USER_NUMBER as PrimaryReviewer_USER_NUMBER, pr.FIRST_NAME as PrimaryReviewer_FIRST_NAME, -- etc
        sr.INTERNAL_USER_NUMBER as SecondaryReviewer_USER_NUMBER, sr.FIRST_NAME as SecondaryReviewer_FIRST_NAME, -- etc
        da.INTERNAL_USER_NUMBER as DmcpAnalyst_USER_NUMBER, da.FIRST_NAME as DmcpAnalyst_FIRST_NAME -- etc
    FROM dbo.A2F_DMCP_0005_BrowseTest() -- Assuming this is your complex query or view
    

    For the splitOn parameter, you now specify the first aliased column of each object to split on. just make sure these names match exactly what you've aliased them to in your SQL query.

      var certificationReviews2 = await connection.QueryAsync<ARMS_API.Models.CertificationReview, State, ARMS_API.Models.Program, ActuarialFirm, dynamic, dynamic, dynamic, dynamic, ARMS_API.Models.CertificationReview>(
        "your_modified_sql_here",
        (certificationReview, state, program, actuarialFirm, certifyingActuaryDynamic, primaryReviewerDynamic, secondaryReviewerDynamic, dmcpAnalystDynamic) => {
            // Map State, Program, ActuarialFirm as before
            certificationReview.STATE = state;
            certificationReview.STATE.PROGRAMS.Add(program);
            certificationReview.ACTUARIAL_FIRM = actuarialFirm;
            
            // Manually map each User, converting 'dynamic' to 'User' and assigning aliased fields to properties
            certificationReview.ACTUARIAL_FIRM.CERTIFYING_ACTUARY.Add(new User {
                INTERNAL_USER_NUMBER = certifyingActuaryDynamic.CertifyingActuary_USER_NUMBER,
                FIRST_NAME = certifyingActuaryDynamic.CertifyingActuary_FIRST_NAME,
                // map other properties similarly
            });
    
            // Repeat for primaryReviewer, secondaryReviewer, and dmcpAnalyst
            
            return certificationReview;
        },
        splitOn: "STATE_ID,PROGRAM_DD,ACTUARIAL_FIRM_DD,CertifyingActuary_USER_NUMBER,PrimaryReviewer_USER_NUMBER,SecondaryReviewer_USER_NUMBER,DmcpAnalyst_USER_NUMBER"
    ).ConfigureAwait(false);