Skip to content

Commit

Permalink
use the new Signum.MSBuildTask and Signum.Analyzer nugets
Browse files Browse the repository at this point in the history
  • Loading branch information
olmobrutall committed Aug 11, 2019
1 parent bac25bd commit c99d4da
Show file tree
Hide file tree
Showing 6 changed files with 17 additions and 63 deletions.
11 changes: 2 additions & 9 deletions Signum.Engine/Signum.Engine.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Signum.Analyzer" Version="2.0.0" />
<!--<PackageReference Include="Signum.MSBuildTask" Version="1.0.3" />-->
<PackageReference Include="Signum.Analyzer" Version="2.3.0" />
<PackageReference Include="Signum.MSBuildTask" Version="1.0.5" />
</ItemGroup>

<ItemGroup>
Expand All @@ -22,11 +22,4 @@
<ProjectReference Include="..\Signum.Entities\Signum.Entities.csproj" />
<ProjectReference Include="..\Signum.Utilities\Signum.Utilities.csproj" />
</ItemGroup>

<Target Name="SignumAfterCompile" AfterTargets="AfterCompile" Outputs="$(TargetPath)">
<WriteLinesToFile File="$(BaseIntermediateOutputPath)SignumReferences.txt" Lines="@(ReferencePath)" Overwrite="true" Encoding="Unicode" />
<Exec command="dotnet &quot;D:\Signum\southwind\Framework\Signum.MSBuildTask\bin\x64\Debug\netcoreapp2.1\Signum.MSBuildTask.dll&quot; &quot;@(IntermediateAssembly)&quot; &quot;$(BaseIntermediateOutputPath)SignumReferences.txt&quot;" ConsoleToMSBuild="false">
<Output TaskParameter="ConsoleOutput" PropertyName="OutputOfExec" />
</Exec>
</Target>
</Project>
11 changes: 2 additions & 9 deletions Signum.Entities/Signum.Entities.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Signum.Analyzer" Version="2.0.0" />
<!--<PackageReference Include="Signum.MSBuildTask" Version="1.0.3" />-->
<PackageReference Include="Signum.Analyzer" Version="2.3.0" />
<PackageReference Include="Signum.MSBuildTask" Version="1.0.5" />
</ItemGroup>

<ItemGroup>
Expand All @@ -23,11 +23,4 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>

<Target Name="SignumAfterCompile" AfterTargets="AfterCompile" Outputs="$(TargetPath)">
<WriteLinesToFile File="$(BaseIntermediateOutputPath)SignumReferences.txt" Lines="@(ReferencePath)" Overwrite="true" Encoding="Unicode" />
<Exec command="dotnet &quot;D:\Signum\southwind\Framework\Signum.MSBuildTask\bin\x64\Debug\netcoreapp2.1\Signum.MSBuildTask.dll&quot; &quot;@(IntermediateAssembly)&quot; &quot;$(BaseIntermediateOutputPath)SignumReferences.txt&quot;" ConsoleToMSBuild="false">
<Output TaskParameter="ConsoleOutput" PropertyName="OutputOfExec" />
</Exec>
</Target>
</Project>
1 change: 0 additions & 1 deletion Signum.React/Scripts/Signum.Entities.Basics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,6 @@ export interface TypeEntity extends Entities.Entity {
cleanName: string;
namespace: string;
className: string;
fullClassName: string;
}


31 changes: 4 additions & 27 deletions Signum.React/Signum.React.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.App" />
<!--<PackageReference Include="Microsoft.TypeScript.MSBuild" Version="3.5.3">
<PackageReference Include="Microsoft.TypeScript.MSBuild" Version="3.5.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>-->
</PackageReference>
</ItemGroup>

<ItemGroup>
Expand All @@ -45,8 +45,8 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Signum.Analyzer" Version="2.0.0" />
<!--<PackageReference Include="Signum.TSGenerator" Version="2.0.5" />-->
<PackageReference Include="Signum.Analyzer" Version="2.3.0" />
<PackageReference Include="Signum.TSGenerator" Version="2.0.5" />
</ItemGroup>

<ItemGroup>
Expand All @@ -63,27 +63,4 @@
<TypeScriptCompile Include="**\*.tsx" />
<TypeScriptCompile Include="**\*.ts" />
</ItemGroup>

<!--<Target Name="SignumAfterCompile" AfterTargets="AfterCompile" Outputs="$(TargetPath)">
<WriteLinesToFile File="$(BaseIntermediateOutputPath)SignumReferences.txt" Lines="@(ReferencePath)" Overwrite="true" Encoding="Unicode" />
<Exec command="dotnet &quot;D:\Signum\southwind\Framework\Signum.MSBuildTask\bin\Debug\netcoreapp2.1\Signum.MSBuildTask.dll&quot; &quot;@(IntermediateAssembly)&quot; &quot;$(BaseIntermediateOutputPath)SignumReferences.txt&quot;" ConsoleToMSBuild="false">
<Output TaskParameter="ConsoleOutput" PropertyName="OutputOfExec" />
</Exec>
</Target>-->

<Target Name="GenerateSignumTS">
<WriteLinesToFile File="$(BaseIntermediateOutputPath)SignumReferences.txt" Lines="@(ReferencePath)" Overwrite="true" Encoding="Unicode" />
<WriteLinesToFile File="$(BaseIntermediateOutputPath)SignumContent.txt" Lines="@(Content);@(None)" Overwrite="true" Encoding="Unicode" />
<Exec command="dotnet &quot;D:\Signum\southwind\Framework\Signum.TSGenerator\bin\Debug\netcoreapp2.1\Signum.TSGenerator.dll&quot; &quot;@(IntermediateAssembly)&quot; &quot;$(BaseIntermediateOutputPath)SignumReferences.txt&quot; &quot;$(BaseIntermediateOutputPath)SignumContent.txt&quot;" ConsoleToMSBuild="false">
<Output TaskParameter="ConsoleOutput" PropertyName="OutputOfExec" />
</Exec>
</Target>

<PropertyGroup>
<CompileTypeScriptDependsOn>
GenerateSignumTS;
$(CompileTypeScriptDependsOn);
</CompileTypeScriptDependsOn>
<ApplicationIcon />
</PropertyGroup>
</Project>
11 changes: 2 additions & 9 deletions Signum.Test/Signum.Test.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="2.2.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" />
<PackageReference Include="Signum.Analyzer" Version="2.0.0" />
<PackageReference Include="Signum.Analyzer" Version="2.3.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
<PrivateAssets>all</PrivateAssets>
Expand All @@ -35,7 +35,7 @@
</ItemGroup>

<ItemGroup>
<!--<PackageReference Include="Signum.MSBuildTask" Version="1.0.3" />-->
<PackageReference Include="Signum.MSBuildTask" Version="1.0.5" />

</ItemGroup>

Expand All @@ -49,11 +49,4 @@
<ItemGroup>
<Folder Include="Properties\" />
</ItemGroup>

<Target Name="SignumAfterCompile" AfterTargets="AfterCompile" Outputs="$(TargetPath)">
<WriteLinesToFile File="$(BaseIntermediateOutputPath)SignumReferences.txt" Lines="@(ReferencePath)" Overwrite="true" Encoding="Unicode" />
<Exec command="dotnet &quot;D:\Signum\southwind\Framework\Signum.MSBuildTask\bin\x64\Debug\netcoreapp2.1\Signum.MSBuildTask.dll&quot; &quot;@(IntermediateAssembly)&quot; &quot;$(BaseIntermediateOutputPath)SignumReferences.txt&quot;" ConsoleToMSBuild="false">
<Output TaskParameter="ConsoleOutput" PropertyName="OutputOfExec" />
</Exec>
</Target>
</Project>
15 changes: 7 additions & 8 deletions Signum.Utilities/ExpressionExpanderAttributes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,24 +84,23 @@ public ExpressionFieldAttribute(string name)
}
}

/// <summary>
/// Marks a property or method for Signum.MSBuildTask to extract the body into and static field with the expression tree.
/// </summary>
[System.AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)]
public sealed class AutoExpressionFieldAttribute : Attribute
{
public AutoExpressionFieldAttribute()
{
}
}


public static class As
{
/// <summary>
/// In Combination with AutoExpressionFieldAttribute, allows the extraction from an Expression field by Signum.MSBuildTask.
/// In Combination with AutoExpressionFieldAttribute, allows the extraction of 'body' expression into an static field (by Signum.MSBuildTask) so the method can be consumed by the LINQ provider and translated to SQL.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="exp"></param>
/// <typeparam name="T">return type</typeparam>
/// <param name="body">The implementation of the property or method</param>
/// <returns></returns>
public static T Expression<T>(Expression<Func<T>> exp)
public static T Expression<T>(Expression<Func<T>> body)
{
throw new InvalidOperationException("This method is not meant to be called. Missing reference to Signum.MSBuildTask in this assembly?");
}
Expand Down

2 comments on commit c99d4da

@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.

New [AutoExpressionField] (and [ExpressionField("auto")] removed)

Writing methods and properties as expression tree instead of normal IL code is necessary so they can be translated to SQL.

Signum Framework uses this very often:

  • For defining ToString methods that can be executed in the database (for retrieving Lite<T> efficiently) using toStringExpression snippet
  • To navigate foreign key backwards using expressionMethodQuery.
  • ...to encapsulate any custom expression that could be useful to define chars, word templates, etc (typically using expressionProperty or expressionMethod).

The old [ExpressionField]

Since C# has not direct support for writing expression methods and properties, we have typically done like this:

//toStringExpression
static Expression<Func<WordTemplateEntity, string>> ToStringExpression = @this => @this.Name;
[ExpressionField]
public override string ToString()
{
    return ToStringExpression.Evaluate(this);
}

//expressionMethodQuery
static Expression<Func<Entity, IQueryable<AlertEntity>>> AlertsExpression =
    e => Database.Query<AlertEntity>().Where(a => a.Target.Is(e));
[ExpressionField]
public static IQueryable<AlertEntity> Alerts(this Entity e)
{
    return AlertsExpression.Evaluate(e);
}

//expressionProperty
static Expression<Func<AlertEntity, bool>> AlertedExpression =
    a => !a.AttendedDate.HasValue && a.AlertDate <= TimeZoneManager.Now;
[ExpressionField]
public bool Alerted
{
    get { return AlertedExpression.Evaluate(this); }
}

This pattern has been usefull for many yars, but has some problems:

  • When getting introduced to Signum Framework, you need to understand what and expression tree is too early.
  • You need to declare the signature of the method twice, once in the method or property (with the return type at the beginning) and one in the expression field with the implementation (with the return type at the end). The two signatures should be identical or the LINQ provider doesn't know what to do.
  • You need to include the implicit 'this' parameter for instance methods and properties, typically named @this inf the expression field.
  • Has problems with overloads, since you can not have two fields with the same name.
  • Has problems with generic methods, since you can not have generic fields.

Still, thanks to the snippets, was relatively easy to write expression properties and method, but the code looked bloated anyway, making it hared to read.

After some years, a roslyn analyzer was created in Signum.Analyzer to produce a compile time error when the two declarations get out of sync.

Additionally, some code in Signum.MSBuildTask was introduced to parse the body of the methods or properties decorated with [ExpressionField] (aka: `[ExpressionField("auto")]) and extract the name of the field, writing it automatically it in the attribute argument. This somehow fixed the overload problem with a little bit more of complexity :S.

The end result is a lot of complication for something that should be easier.

The new [AutoExpressionField]

Here is how you write the same now:

//toStringExpression
[AutoExpressionField]
public override string ToString() => As.Expression(() => Name);

//expressionMethodQuery
[AutoExpressionField]
public static IQueryable<AlertEntity> Alerts(this Entity e) => 
    As.Expression(() => Database.Query<AlertEntity>().Where(a => a.Target.Is(e)));

//expressionProperty
[AutoExpressionField]
public bool Alerted => 
    As.Expression(() => !AttendedDate.HasValue && AlertDate <= TimeZoneManager.Now);

Awesome right? Just decorate the property of method with [AutoExpressionField] and surround your expression with As.Expression(() => ). It almost looks like direct C# support for it!.

This is how AutoExpressionField and As.Expression are implemented:

    /// <summary>
    /// Marks a property or method for Signum.MSBuildTask to extract the body into and static field with the expression tree. 
    /// </summary>
    [System.AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)]
    public sealed class AutoExpressionFieldAttribute : Attribute
    {
    }

    public static class As
    {
        /// <summary>
        /// In Combination with AutoExpressionFieldAttribute, allows the extraction of 'body' expression into an static field (by Signum.MSBuildTask) so the method can be consumed by the LINQ provider and translated to SQL.
        /// </summary>
        /// <typeparam name="T">return type</typeparam>
        /// <param name="body">The implementation of the property or method</param>
        /// <returns></returns>
        public static T Expression<T>(Expression<Func<T>> body)
        {
            throw new InvalidOperationException("This method is not meant to be called. Missing the reference to Signum.MSBuildTask in this assembly?");
        }
    }

... and this took 10 years to be invented? Well the real magic happens in Signum.MSBuildTaks, that is able to read the IL in the body method and transform it, so when you write:

class ProductEntity : Entity
{ 
    public string FirstName { get; set; }    
    public string LastName { get; set; }
    
    [AutoExpressionField]
    public string GetName(bool full) => As.Expression(() => full ? (FirstName + " " + LastName) : FirstName);
}

it converts it to something like:

class ProductEntity : Entity
{
    public string FirstName { get; set; }    
    public string LastName { get; set; }
    
    static ProductEntity()
    {
    	GetNameInit();
    }
    
    static GetNameInit()
    {
    	GetNameExpression = (@this, full) => full ? (@this.FirstName + " " + @this.LastName) : @this.FirstName;
    }
    
    static Expression<ProductEntity, bool, string> GetNameExpression;
    [ExpressionField("GetNameExpression")]
    public string GetName(bool full) => GetNameExpression.Evaluate(this, full);
}

Note how is able to:

  • Create GetNameExpression static field with the implicit ProductEntity this argument and the bool full.
  • Create a GetNameInit method that will be called onece in the static constructor.
  • Extract the expression tree inside As.Expression, and move it to GetNameInit. This is the most complicated part because it requires parsing the IL that constructs the expression tree and modify it, because what before where closures to the parameters in the top-most method (GetName) now have to be converted in direct parameters of the Expression<Func<...>>.
  • Replace the body of GetName with a call to GetNameExpression.Evaluate.
  • Replace the AutoExpressionFieldAttribute with a ExpressionFieldAttribute("GetNameExpression").

That's what took 10 years to be invented!

Changes in snippets

The snippets that you're used to (expressionToString, expressionProperty, expressionMethod, expressionMethodQuery) are still there, now producing the new simpler code.

Changes Signum.Analyzer

Also, Signum.Analyzer is vigilant so you don't forget using As.Expression(()=> ) in the body of a member decorated ith AutoExpressionField and suggests to add it when not.

So you can write just write

public static IQueryable<OrderEntity> Orders(this ProductEntity p) => 
	Database.Query<OrderEntity>().Where(a => a.Details.Any(d => d.Product.Is(p)));

When you add [AutoExpressionField] you get a compile-time error, then use the Quick Fix and you get:

[AutoExpressionField]
public static IQueryable<OrderEntity> Orders(this ProductEntity p) =
    As.Expression(() => Database.Query<OrderEntity>().Where(a => a.Details.Any(d => d.Product.Is(p))));

How to change the code

While the new AutoExpressionField is lowered to the old ExpressionField, the part of the MSBuildTask that was replacing [ExpressionField] by [ExpressionField("YourExpression"]` now seem redundant, so I've removed it.

This means AutoExpressionField is a breaking change and enjoying it is non-optional :).

Update Framework and Extensions, and update the Nugets of your projects, specifically the Signum ones. If you get the downgrade error in VS edit manually the .csproj.

Then apply this renames in your .Entities and .Logic assemblies.

For expressionToString
Find: ( *)(public )?static (readonly )?Expression.*=\s*[\w ()@]+=> *(.*);\s*\r?\n?\s*\[ExpressionField\]\s*\r?\n?\s*(public override string ToString\(\))\s*\r?\n?\s*\{\s*return .*;\s*}
Repl: $1[AutoExpressionField]\r\n$1$5 => As.Expression(() => $4);

For expressionMethod / expressionMethodQuery
Find: ( *)(public )?static (readonly )?Expression.*=\s*[\w ()@]+=> *(.*);\s*\n?\s*\[ExpressionField\]\s*(public.*\))\s*\{\s*return .*;\s*}
Repl: $1[AutoExpressionField]\r\n$1$5 => \r\n$1    As.Expression(() => $4);

For expressionProperty
Find: ( *)(public )?static (readonly )?Expression.*=\s*[\w ()@]+=> *(.*);\s*\n?\s*\[ExpressionField\]\s*(public.*)\s*\{\s*get\s*\{\s*return .*;\s*}\s*}
Repl: $1[AutoExpressionField]\r\n$1$5 => As.Expression(() => $4);

Note: Manual fixes may be needed after the regex is executed if the parameters of the expression field and the member (method or property) are different, or when 'this' is involved.

Hope the transition is easy and you can enjoy the new syntax soon.

Cheers!

@MehdyKarimpour
Copy link
Contributor

Choose a reason for hiding this comment

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

Awesome. 😀

Please sign in to comment.