Skip to content

Commit

Permalink
Merge remote-tracking branch 'tfs/liteModel'
Browse files Browse the repository at this point in the history
  • Loading branch information
olmobrutall committed Jun 26, 2022
2 parents 2e17245 + 498aea3 commit 160ddc3
Show file tree
Hide file tree
Showing 146 changed files with 2,566 additions and 1,390 deletions.
10 changes: 5 additions & 5 deletions Signum.Engine.Extensions/Cache/CacheLogic.cs
Original file line number Diff line number Diff line change
Expand Up @@ -541,21 +541,20 @@ public override IEnumerable<PrimaryKey> GetAllIds()
return cachedTable.GetAllIds();
}

public override string GetToString(PrimaryKey id)
public override object GetLiteModel(PrimaryKey id, Type modelType, IRetriever retriever)
{
AssertEnabled();

return cachedTable.GetToString(id);
return cachedTable.GetLiteModel(id, modelType, retriever);
}

public override string? TryGetToString(PrimaryKey? id)
public override object? TryGetLiteModel(PrimaryKey? id, Type modelType, IRetriever retriever)
{
AssertEnabled();

return cachedTable.TryGetToString(id!.Value)!;
return cachedTable.TryGetLiteModel(id!.Value, modelType, retriever)!;
}


public override bool Exists(PrimaryKey id)
{
AssertEnabled();
Expand Down Expand Up @@ -589,6 +588,7 @@ public override List<T> RequestByBackReference<R>(IRetriever retriever, Expressi
return ids.Select(id => retriever.Complete<T>(id, e => this.Complete(e, retriever))!).ToList();
}



public Type Type
{
Expand Down
257 changes: 257 additions & 0 deletions Signum.Engine.Extensions/Cache/CachedLiteTable.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
using Signum.Utilities.Reflection;
using Signum.Engine.Linq;
using System.Data;
using Signum.Entities.Internal;
using Signum.Engine.Connection;

namespace Signum.Engine.Cache;

class CachedLiteTable<T> : CachedTableBase where T : Entity
{
public override IColumn? ParentColumn { get; set; }

Table table;

Alias currentAlias;
string lastPartialJoin;
string? remainingJoins;

Func<FieldReader, object> rowReader = null!;
ResetLazy<Dictionary<PrimaryKey, object>> rows = null!;
Func<object, PrimaryKey> idGetter;
Dictionary<Type, ICachedLiteModelConstructor> liteModelConstructors = null!;

SemiCachedController<T>? semiCachedController;

public Dictionary<PrimaryKey, object> GetRows()
{
return rows.Value;
}

public CachedLiteTable(ICacheLogicController controller, AliasGenerator aliasGenerator, string lastPartialJoin, string? remainingJoins)
: base(controller)
{
this.table = Schema.Current.Table(typeof(T));

HashSet<IColumn> columns = new HashSet<IColumn> { table.PrimaryKey };

foreach (var modelType in Lite.GetAllLiteModelTypes(typeof(T)))
{
var modelConstructor = Lite.GetModelConstructorExpression(typeof(T), modelType);

ToStringColumnsFinderVisitor.GatherColumns(modelConstructor, this.table, columns);
}

var ctr = this.Constructor = new CachedTableConstructor(this, aliasGenerator, columns.ToList());

this.lastPartialJoin = lastPartialJoin;
this.remainingJoins = remainingJoins;
this.currentAlias = aliasGenerator.NextTableAlias(table.Name.Name);
var isPostgres = Schema.Current.Settings.IsPostgres;

//Query
using (ObjectName.OverrideOptions(new ObjectNameOptions { AvoidDatabaseName = true }))
{
string select = "SELECT {0}\r\nFROM {1} {2}\r\n".FormatWith(
ctr.columns.ToString(c => currentAlias + "." + c.Name.SqlEscape(isPostgres), ", "),
table.Name.ToString(),
currentAlias.ToString());

select += this.lastPartialJoin + currentAlias + "." + table.PrimaryKey.Name.SqlEscape(isPostgres) + "\r\n" + this.remainingJoins;

query = new SqlPreCommandSimple(select);
}

//Reader
{
rowReader = ctr.GetRowReader();

idGetter = ctr.GetPrimaryKeyGetter((IColumn)table.PrimaryKey);
}

rows = new ResetLazy<Dictionary<PrimaryKey, object>>(() =>
{
return SqlServerRetry.Retry(() =>
{
CacheLogic.AssertSqlDependencyStarted();

Dictionary<PrimaryKey, object> result = new Dictionary<PrimaryKey, object>();

using (MeasureLoad())
using (Connector.Override(Connector.Current.ForDatabase(table.Name.Schema?.Database)))
using (var tr = Transaction.ForceNew(IsolationLevel.ReadCommitted))
{
if (CacheLogic.LogWriter != null)
CacheLogic.LogWriter.WriteLine("Load {0}".FormatWith(GetType().TypeName()));

Connector.Current.ExecuteDataReaderOptionalDependency(query, OnChange, fr =>
{
var obj = rowReader(fr);
result[idGetter(obj)] = obj; //Could be repeated joins
});
tr.Commit();
}

return result;
});
}, mode: LazyThreadSafetyMode.ExecutionAndPublication);

if (!CacheLogic.WithSqlDependency) //Always semi
{
semiCachedController = new SemiCachedController<T>(this);
}
}

public override void SchemaCompleted()
{
this.liteModelConstructors = Lite.GetAllLiteModelTypes(typeof(T))
.ToDictionary(modelType => modelType, modelType =>
{
var modelConstructor = Lite.GetModelConstructorExpression(typeof(T), modelType);
var cachedModelConstructor = LiteModelExpressionVisitor.giGetCachedLiteModelConstructor.GetInvoker(typeof(T), modelType)(this.Constructor, modelConstructor);
return cachedModelConstructor;
});

if (this.subTables != null)
foreach (var item in this.subTables)
item.SchemaCompleted();
}

protected override void Reset()
{
if (rows == null)
return;

if (CacheLogic.LogWriter != null )
CacheLogic.LogWriter.WriteLine((rows.IsValueCreated ? "RESET {0}" : "Reset {0}").FormatWith(GetType().TypeName()));

rows.Reset();
}

protected override void Load()
{
if (rows == null)
return;

rows.Load();
}


public Lite<T> GetLite(PrimaryKey id, IRetriever retriever, Type modelType)
{
Interlocked.Increment(ref hits);

var model = liteModelConstructors.GetOrThrow(modelType).GetModel(id, retriever);

var lite = Lite.Create<T>(id, model);
retriever.ModifiablePostRetrieving((LiteImp)lite);
return lite;
}

public override int? Count
{
get { return rows.IsValueCreated ? rows.Value.Count : (int?)null; }
}

public override Type Type
{
get { return typeof(Lite<T>); }
}

public override ITable Table
{
get { return table; }
}

class ToStringColumnsFinderVisitor : ExpressionVisitor
{
ParameterExpression param;

HashSet<IColumn> columns;

Table table;

public ToStringColumnsFinderVisitor(ParameterExpression param, HashSet<IColumn> columns, Table table)
{
this.param = param;
this.columns = columns;
this.table = table;
}

public static Expression GatherColumns(LambdaExpression lambda, Table table, HashSet<IColumn> columns)
{
ToStringColumnsFinderVisitor toStr = new ToStringColumnsFinderVisitor(
lambda.Parameters.SingleEx(),
columns,
table
);

var result = toStr.Visit(lambda.Body);

return result;
}



static MethodInfo miMixin = ReflectionTools.GetMethodInfo((Entity e) => e.Mixin<MixinEntity>()).GetGenericMethodDefinition();

protected override Expression VisitMember(MemberExpression node)
{
if (node.Expression == param)
{
var field = table.GetField(node.Member);
var column = GetColumn(field);
columns.Add(column);
return node;
}

if (node.Expression is MethodCallExpression me && me.Method.IsInstantiationOf(miMixin))
{
var type = me.Method.GetGenericArguments()[0];
var mixin = table.Mixins!.GetOrThrow(type);
var field = mixin.GetField(node.Member);
var column = GetColumn(field);
columns.Add(column);
return node;
}

return base.VisitMember(node);
}

protected override Expression VisitMethodCall(MethodCallExpression node)
{
var obj = base.Visit(node.Object);
var args = base.Visit(node.Arguments);

LambdaExpression? lambda = ExpressionCleaner.GetFieldExpansion(obj?.Type, node.Method);

if (lambda != null)
{
var replace = ExpressionReplacer.Replace(Expression.Invoke(lambda, obj == null ? args : args.PreAnd(obj)));

return this.Visit(replace);
}

if (node.Object == param && node.Method.Name == nameof(node.ToString))
{
columns.Add(this.table.ToStrColumn!);
return node;
}

return base.VisitMethodCall(node);
}

private IColumn GetColumn(Field field)
{
if (field is FieldPrimaryKey || field is FieldValue || field is FieldTicks)
return (IColumn)field;

throw new InvalidOperationException("{0} not supported when caching the ToString for a Lite of a transacional entity ({1})".FormatWith(field.GetType().TypeName(), this.table.Type.TypeName()));
}
}

internal override bool Contains(PrimaryKey primaryKey)
{
return this.rows.Value.ContainsKey(primaryKey);
}
}
Loading

5 comments on commit 160ddc3

@olmobrutall
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Presenting Lite Model

This commit merges some important changes that have taking almost a week from me an @JafarMirzaie working together. About 1000 lines changed and about 100 fields modified just in Framework.

All to get this:

image

You see this little round faces... took 1000 lines of code :)

Of course this could be done before using formatters and hiddenColumns in the QuerySetting, but the new solution is better because when you add a filter the face also goes up.

image

and the autocomplete

image

Or inside of the entity

image

How it works in 3 simple steps.

  1. Create a model to transport the data in your Entities assembly:
//Employe.cs

public class EmployeeLiteModel : ModelEntity
{
    [StringLengthValidator(Min = 3, Max = 20)]
    public string LastName { get; set; }

    [StringLengthValidator(Min = 3, Max = 10)]
    public string FirstName { get; set; }

    public Lite<FileEntity>? Photo { get; set; } //2

    //Remember to override ToString() for Lite Model!!
    [AutoExpressionField]
    public override string ToString() => As.Expression(() => $"{FirstName} {LastName}"); 
}
  1. Register the Lite Model Constructor in Logic. Just an Expresson<T> determining the necessary fields.
//EmployeeLogic.cs

Lite.RegisterLiteModelConstructor((EmployeeEntity e) => new EmployeeLiteModel
{
    FirstName = e.FirstName,
    LastName = e.LastName,
    Photo = e.Photo,
});

Note: This Expression<Func<T, M>> will be used when retreiving the lite from the database, as well as when doing ToLite with an in-memory entity. If you want to do operations that could fail in either case (like using Lite<T>.Entity) use the overload taking a LiteModelConstructor so you can pass different code fo the Expression<Func<T, M>> (database) and Func<T, M> (in-memory)

  1. Override EntitySettings.renderLite in the client-side
  // SouthwindClient.tsx

  Navigator.addSettings(new EntitySettings(EmployeeEntity, e => import('./Templates/Employee'), {
    renderLite: (lite, subStr) => {
      if (EmployeeLiteModel.isInstance(lite.model))
        return (
          <span>
            <FileImage
              style={{ width: "20px", height: "20px", borderRadius: "100%", marginRight: "4px", marginTop: "-3px" }}
              file={lite.model.photo} />
            {TypeaheadOptions.highlightedText(lite.model.firstName + " " + lite.model.lastName, subStr)}
          </span>
        );

      if (typeof lite.model == "string")
        return TypeaheadOptions.highlightedText(lite.model, subStr);

      return lite.EntityType;
    }
  }));

This new extension point, renderLite, is currently used when rendering a Lite<T> in a n EntityLine, EntityCombo, EntityLink (used in SearchControl results), SearchValue, etc...

There is also a new renderEntity used for cases where the EntityLine / EntityCombo takes a full entity, but it's not necesary to override it in most cases since if will fall-back to renderLite if needed.

Note: The use of TypeaheadOptions.highlightedText is used to highlight the maching characters when using auto-complete, and just returns the first argument in any other case.

That's it! With just a few lines you can retrieve the necessary information and customize how a Lite<T> is rendered for some custom T everywhere in the application.

Can an entity have more than Lite Model type?

Yes! The model RegisterLiteModelConstructor has an optional parameter isDefault with the default value of true.

public static void RegisterLiteModelConstructor<T, M>(Expression<Func<T, M>> constructorExpression, bool isDefault = true, bool isOverride = false)

This is the most common case, and overrides the framework globally so that every Lite<EmployeeEntity> will come with a EmployeeLiteModel inside.

But when isDefault is set to false, then all the Lite<EmployeeEntity> will continue comming with an string inside, and you can opt-in in a query by doing:

emp.ToLite(typeof(EmployeeLiteModel))

Of, even better, can be overriden for a particular property.

class OrderEntity : Entity 
{
   //...   
   [LiteModelAttribute(typeof(EmployeeLiteModel))]
   public Lite<EmployeeEntity> Employee { get; set; }
   //...
}

If even works with ImplementedBy properties, allowing you to override the Lite Model for each implementation:

class OrderEntity : Entity 
{
    //...
    [LiteModel(typeof(CompanyLiteModel), ForEntityType = typeof(CompanyEntity))]
    [LiteModel(typeof(PersonLiteModel), ForEntityType = typeof(PersonEntity))]
    [ImplementedBy(typeof(CompanyEntity), typeof(PersonEntity))]
    public Lite<CustomerEntity> Customer { get; set; }
    //...
}

Why it took 1000s of lines changed

Server Side (C#)

This change generalizes in Lite<T> the field string? toStr in to object? model. The model is by default an string but if necessary can be type inheriting ModelEntity (if needed, this restriction could be removed in the future).

Internally, this means inportant changes in Database API, the LINQ provider and CacheLogic.

For the end-user the change is transparent in C#, since toStr is private anyway. Just remember that since the model replace the string, you have to override ToString in the model as well!!!

class LiteImp<T> : Lite<T>
{
    //...
    public override string? ToString()
    {
        if (this.entityOrNull != null)
            return this.entityOrNull.ToString();

        return this.model?.ToString();
    }
   //..
}

Client Side (Typescript)

The client side also has some important internal changes, like allowing LambdaToJavascriptConverter to translate the LiteModalConstructor expressions to Javascript to you can do toLite() in the client-side as well.

Unfortunately, in the client-side the change is more disruptive for the end-user, since the field toStr was public.

export interface Lite<T extends Entity> {
  EntityType: string;
  id?: number | string;
  //toStr?: string; //Removed  
  model?: unknown; //Added

  ModelType?: string; //Added
  entity?: T;
}

So we have built a Signum.Upgrade Upgrade_20220623_ToStrGetToString that replaces every occurence like a.b.c.toStr to getToString(a.b.c), but you will need to import getToString manually in every file:

import { getToString } from '@framework/Signum.Entities'

Conclusion

I think this is an important step in helping Signum Framework avoid some of the tradictional limitations, comparable to MixinEntity, PrimaryKey or VirtualMList.

Hopefully if will help you build more user-friendly and intuitive applications.

Enjoy!

@doganc
Copy link

@doganc doganc commented on 160ddc3 Jun 26, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very impressive, perfect

@rezanos
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GREAT framework improvement! 🚀 with Fantastic design 👏👏👌
Hooray! 🎉
Thanks!

@MehdyKarimpour
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great improvement 🥇

@antonioh
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very nice, looking forward to use it!

Please sign in to comment.