Home > Blockchain >  is it a good idea to do transformation in a database model with ASP.Net
is it a good idea to do transformation in a database model with ASP.Net

Time:12-20

we are a small development team. We develop in ASP.NET and we are starting to use generic controllers and services.

The goal is to have solid methods for things that are repetitive.

What we ask ourselves is if it is a good idea to do some transformation in the data models to allow us to reuse our functions that we know are working?

Exemple: we have a combobox and we want to manage the display and search. It's always the same and redundant.

This is my class

[Table("stage.Test")]
public partial class Test : IBaseEntity, ICombobox
{

    public virtual Product Product { get; set; }
    public  string nom { get; set; }
    public  string  prenom { get; set; }
    public  string title { get; set; }

    [NotMapped]
    public virtual string AffichageCombobox => nom   prenom;
    [NotMapped]
    public virtual string TexteRecherche => Product.Gabarit.Description;

 }

as you can see i have two columns with the tag [NotMapped]. These are the columns in the interface ICombobox

    public interface ICombobox
{
    string AffichageCombobox { get;}
    string TexteRecherche { get; }
}

this is the first service where I use one of my two columns which redirects to other columns. [We use the column "AffichageCombobox" from the model]

        public List<ComboboxViewModel> GetComboboxViewModel(int? id, bool actifseulement, string text)
    {
        var query = _requestDatabaseService.GetComboboxQuery<T>(id, actifseulement, text);
        var list = query.Select(table => new ComboboxViewModel
        {
            Id = table.Id,
            AffichageCombobox = table.DateHFin == null ? table.AffichageCombobox : table.AffichageCombobox   " (inactif)"
        }).ToList();

        return list;
    }   

This is the RequestDatabaseService [We use the column "TexteRecherche" from the model]

        public List<T> GetComboboxQuery<T>(int? id, bool actifseulement, string text) where T : class, IBaseEntity, ICombobox
    {
        text = text.ToLower();

        var list = _dbContext.Set<T>()
            .If(id.HasValue,
                q => q.Where(x => x.Id == id))
            .If(actifseulement,
                q => q.Where(x => x.DateHFin == null))
            .If(text != "",
                q => q.Where(x => x.TexteRecherche.ToLower() == text))
            .ToList();
        return list;
    }

As you can see, I am using an interface to add columns to redirect to the correct columns to my data model to avoid overriding my methods for two column.

Is it a good idea, a good practice ?

What do you think is the best practice if we want to do generic functions, but the columns are not called the same way?

Thank you!

CodePudding user response:

Your solution has a lot of weaknesses

  1. You have extended Model to handle specific UI cases. In my opinion it is bad practice.
  2. Your virtual properties will not work in LINQ query. EF translates only Expression because it canot look into compiled property body.

What we can do here is simplifying of building such comboboxes. I have defind set fo extensions which can be reused for such scenarios. Sorry if there some mistakes, written from memory.

How it can be used:

Assuming that GetComboboxViewModel is not in generic class

public List<ComboboxViewModel> GetComboboxViewModel(int? id, bool actifseulement, string text)
{
    // uncover DbContext. All that we need is IQueryable<Test>
    var ctx = _requestDatabaseService.GetContext();
    var query = ctx.Test.AsQueryable();

    var comboItems = query
        .FilterItems(id, actifseulement)
        .GetComboboxQuery(text, e => e.Product.Gabarit.Description, e => e.nom   e.prenom)
        .ToList();

    return comboItems;
}

Think about this solution and yes, we can register somewhere pair of Lmbdas Dictionary<Type, (LambdaExpression: searchProp, LambdaExpression: displayProp)> and dynamically build call above.

Realisation:

public static class QueryableExtensions
{ 
    // more simlified version for filtering
    public static IQueryable<T> WhereIf(this IQueryable<T> query, bool condition, Expression<Func<T, bool>> predicate)
    {
        return condition ? query.Where(predicate) : query;
    }

    // handy function for filtering
    public static IQueryable<T> FilterItems<T>(this IQueryable<T> query, int? id, bool onlyActive) 
        where T : IBaseEntity
    {
        query = query
            .WhereIf(id.HasValue, x => x.Id == id)
            .WhereIf(onlyActive, x => x.DateHFin == null)

        return query;
    }

    // dynamic generationg filtering and projection
    public static IQueryable<ComboboxViewModel> GetComboboxQuery<T>(this IQueryable<T> query, string text, Expression<Func<T, string>> searchProp, Expression<Func<T, string>> dsiplayProp) 
        where T : IBaseEntity
    {
        if (!string.IsNullOrEmpty(text))
        {
            text = text.ToLower();

            // defining search pattern
            // this also extension point, you may use here `Contains` or FullText search functions
            Expression<Func<string, string, bool>> filterFunc = (s, t) => s.ToLower() == t;

            // reusing parameter from searchProp lambda
            var param = searchProp.Parameters[0];

            // applying pattern to searchprop
            var filterBody = ExpressionReplacer.GetBody(filterFunc, searchProp.Body, Expression.Constant(text));

            // applying generated filter
            var filterPredicate = Expression.Lambda<Func<T, bool>>(filterBody, param);
            query = query.Where(filterPredicate);
        }

        // defining template for Select
        Expression<Func<T, string, ComboboxViewModel>> createTemplate = (entity, dp) => new ComboboxViewModel
        {
            Id = entity.Id,
            AffichageCombobox = entity.DateHFin == null ? dp : dp   " (inactif)"
        };

        // reusing parameter from dsiplayProp lambda
        var entityParam = dsiplayProp.Parameters[0];

        // injecting dsiplayProp into createTemplate
        var selectBody = ExpressionReplacer.GetBody(createTemplate, entityParam, dsiplayProp.Body);

        var selectLambda = Expression.Lambda<Func<T, ComboboxViewModel>>(selectBody, entityParam);

        // applying projection
        var comboQuery = query.Select(selectLambda);
        return comboQuery;
    }

    // helper class for correcting expressions
    class ExpressionReplacer : ExpressionVisitor
    {
        readonly IDictionary<Expression, Expression> _replaceMap;

        public ExpressionReplacer(IDictionary<Expression, Expression> replaceMap)
        {
            _replaceMap = replaceMap ?? throw new ArgumentNullException(nameof(replaceMap));
        }

        public override Expression Visit(Expression exp)
        {
            if (exp != null && _replaceMap.TryGetValue(exp, out var replacement))
                return replacement;
            return base.Visit(exp);
        }

        public static Expression Replace(Expression expr, Expression toReplace, Expression toExpr)
        {
            return new ExpressionReplacer(new Dictionary<Expression, Expression> { { toReplace, toExpr } }).Visit(expr);
        }

        public static Expression Replace(Expression expr, IDictionary<Expression, Expression> replaceMap)
        {
            return new ExpressionReplacer(replaceMap).Visit(expr);
        }

        public static Expression GetBody(LambdaExpression lambda, params Expression[] toReplace)
        {
            if (lambda.Parameters.Count != toReplace.Length)
                throw new InvalidOperationException();

            return new ExpressionReplacer(Enumerable.Range(0, lambda.Parameters.Count)
                .ToDictionary(i => (Expression) lambda.Parameters[i], i => toReplace[i])).Visit(lambda.Body);
        }
    }
}

Well, after writing this sample, I think, it can be cardinally simplified by using LINQKit. Will post another answer with LINQKit usage if you are interested,

  • Related