简体   繁体   中英

C# Generic binding

I am working on an assignment for school and trying to implement as much features just for learning sake. Hence I've made a generic mapper that maps databse tables to objects to see what's possible. The Db in this case is local. I know I'm making loads and loads of calls and should go around this very differently but....

Everything works as intended except for when a class has a Collection of another class.

Example:

class Student {

public int Id { get; set; }
public string Name { get; set; }
}

My method for filling a list of all the students in the database.

public List<TModel> MapEntitiesFromDb<TModel>(string tablename, string customquery = "") where TModel : class, new() 
    {
        try
        {
            sql = ValidateSelectSql(tablename, customquery);
        }
        catch (AccessViolationException ex) { Console.WriteLine(ex.Message); }

        command.CommandText = sql;
        command.Connection = conn;

        List<TModel> list = new List<TModel>();
        try
        {
            using (conn)
            {
                Type t = new TModel().GetType();

                conn.Open();
                using (reader = command.ExecuteReader())
                {

                    if (t.GetProperties().Length != reader.FieldCount)
                        throw new Exception("There is a mismatch between the amount of properties and the database columns. Please check the input code and try again.");

                    //Possible check is to store each column and property name in arrays and match them to a new boolean array, if there's 1 false throw an exception.
                    string columnname;
                    string propertyname;

                    //Pairing properties with columns 
                    while (reader.Read())
                    {
                        TModel obj = new TModel();

                        for (int i = 0; i < reader.FieldCount; i++)
                        {
                            columnname = reader.GetName(i).ToString().ToLower();

                            PropertyInfo[] properties = t.GetProperties();
                            foreach (PropertyInfo propertyinfo in properties)
                            {
                                propertyname = propertyinfo.Name.ToLower();

                                if (propertyname == columnname)
                                {
                                    propertyinfo.SetValue(obj, reader.GetValue(i));
                                    break;
                                }
                            }
                        }

                        list.Add(obj);

                    }
                }
            }
        }
        catch (Exception ex) { Console.WriteLine(ex.Message); }

        return list;
    }

My ValidateSelectSql just returns the sql string that needs to be used in the query.

After calling:

List<Student> = MapEntitiesFromDb<Student>("students");

It will return a list with all the students like intended.

Things go wrong when I add a collection for example:

class Student {

public Student()
{
    this.Courses = new List<Course>();

    string customsqlquery = ::: this works and is tested! :::
    Courses = MapEntitiesFromDb<Course>("", customsqlquery);
}

public int Id { get; set; }
public string Name { get; set; }

public ICollection<Course> Courses;
}

The courses list returned empty and with some help of the debugger tool I found out at the time of creating the object the Id property is 0 of course. In my query I am filtering on student Id but at the time of executing the method to fill the Courses list in the constructor the Id of student will always be 0 becuase it's set at a later stage and the result will be no courses in the list.

I'm wondering if I should put a check for an ICollection property after the other properties are set and if so execute a method on the object that in return executes the method that's now inside the constructor?

I can't call any methods on TModel, else it would be as simple as finding if TModel has a collection property and call obj.FillCollection(); after the Id property has been assigned in the GetEntitiesFromDb method.

I was also thinking about recursion. Again I'd have to find if obj has a collection property and then call GetEntitiesFromDB but it seems undoable because I also need to find out the type in between <> and I Can't send any customquery from the outside...

Maybe tackle it from a whole other perspective?

I can really use some advice on how to tackle this problem.

The most straightforward way to approach this would be to have the collection property lazy load what it needs. I would additionally recommend that you use IEnumerable<T> instead of ICollection<T> because this represents a read-only view of what's currently in the database, nobody should be modifying it in any way.

public class Student
{    
    private readonly Lazy<IEnumerable<Course>> courses;

    public int Id { get; set; }
    public IEnumerable<Course> Courses => this.courses.Value;

    public Student()
    {
        this.courses = new Lazy<IEnumerable<Course>>(LoadCourses);
    }

    private IEnumerable<Course> LoadCourses()
    {
        var sql = "custom SQL query that uses this.Id after it's loaded";
        return MapEntitiesFromDb(sql);
    }
}

I'm only recommending this approach because you mentioned that this is just an academic exercise to help you learn about the tools available to you. In an actual production environment this approach would very quickly become unwieldy and I would recommend using Entity Framework instead (which may be something else that you might want to learn about).

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM