My extreme meta-programming workshop

Today, I was in Tel Aviv speaking about meta-programming for a workshop day.

As promised, Iā€™m publishing the correction of the different exercises.

 

Exercise 1:

The goal of the first was to change the default Edmx T4 to generate

public partial class Customer
{
    private int _id;
    public int Id
    {
        get { return _id; }
        set
        {
            OnIdChanging(ref value);
            _id = value;
            OnIdChanged();
        }
    }
    partial void OnIdChanging(ref int value);
    partial void OnIdChanged();
    private string _name;
    public string Name
    {
        get { return _name; }
        set
        {
            OnNameChanging(ref value);
            _name = value;
            OnNameChanged();
        }
    }
    partial void OnNameChanging(ref string value);
    partial void OnNameChanged();
}

instead of this:

public partial class Customer

{

    public int Id { get; set; }

    public string Name { get; set; }

}

For this, I replaced in the T4 file this code:

    var simpleProperties = typeMapper.GetSimpleProperties(entity);
    if (simpleProperties.Any())
    {
        foreach(var edmProperty in simpleProperties)
        {
#>
    <#=codeStringGenerator.Property(edmProperty)#>
<#
        }
}

by this one:

    var simpleProperties = typeMapper.GetSimpleProperties(entity);
    if (simpleProperties.Any())
    {
        PushIndent(CodeRegion.GetIndent(1));
        foreach (var edmProperty in simpleProperties)
            PropertyWithPartial(edmProperty, typeMapper, code);
        PopIndent();
    }

With the following PropertyWithPartial method:

public void PropertyWithPartial(EdmProperty edmProperty, TypeMapper typeMapper, CodeGenerationTools code)
{
    string fieldName = code.FieldName(edmProperty);
    string propertyName = code.Escape(edmProperty);
    string propertyTypeName = typeMapper.GetTypeName(edmProperty.TypeUsage);
#>
private <#=propertyTypeName#> <#=fieldName#>;
<#=Accessibility.ForProperty(edmProperty)#> <#=propertyTypeName#> <#=propertyName#>
{
    <#=code.SpaceAfter(Accessibility.ForGetter(edmProperty))#>get { return <#=fieldName#>; }
    <#=code.SpaceAfter(Accessibility.ForSetter(edmProperty))#>set
    {
        On<#=propertyName#>Changing(ref value);
        <#=fieldName#> = value;
        On<#=propertyName#>Changed();
    }
}
partial void On<#=propertyName#>Changing(ref <#=propertyTypeName#> value);
partial void On<#=propertyName#>Changed();

<#+
}

 

Exercise 2:

The goal of the second exercise was to analyze the methods of some extension method on a class (only methods and properties).

So from this code:

public class Employee
{
    public string LastName
    {
        get;
        set;
    }

    public string FirstName
    {
        get;
        set;
    }

    public void Foo()
    {
    }
}

public static class C
{
    public static string GetLastName(this Employee e)
    {
        return e.LastName;
    }

    public static void EmployeeFoo(this Employee e)
    {
        e.Foo();
    }

    public static string GetFirstNameAndFoo(this Employee e)
    {
        e.Foo();
        e.Foo();
        return string.Concat(e.LastName, e.FirstName);
    }
}

we wanted to generate (using T4) the following text file:

ClassLibrary1.C.EmployeeFoo(ClassLibrary1.Employee) dependent methods
ClassLibrary1.Employee.Foo()
ClassLibrary1.C.GetFirstNameAndFoo(ClassLibrary1.Employee) dependent methods
ClassLibrary1.Employee.Foo()
ClassLibrary1.C.GetLastName(ClassLibrary1.Employee) dependent properties
ClassLibrary1.Employee.LastName
ClassLibrary1.C.GetFirstNameAndFoo(ClassLibrary1.Employee) dependent properties
ClassLibrary1.Employee.LastName
ClassLibrary1.Employee.FirstName

So this is the T4 template I used:

<#@ template debug="false" hostspecific="true" language="C#" #>
<#@ output extension=".txt" #>
<#@ assembly name="System.Core" #>
<#@ assembly name="Roslyn.Compilers"#>
<#@ assembly name="Roslyn.Compilers.CSharp"#>
<#@ assembly name="Roslyn.Services"#>
<#@ assembly name="Roslyn.Services.CSharp"#>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="Roslyn.Compilers.Common"#>
<#@ import namespace="Roslyn.Compilers.CSharp"#>
<#@ import namespace="Roslyn.Services"#>
<#+
void Analyze(string slnPath, string csProjPath, string filePath, string entityClass, string extensionClass)
{
    var solution = Solution.Load(Path.GetFullPath(Host.ResolvePath(slnPath)));
    var project = solution.Projects.FirstOrDefault(p => p.FilePath == Path.GetFullPath(Host.ResolvePath(csProjPath)));
    var document = project.Documents.First(d => d.FilePath == Path.GetFullPath(Host.ResolvePath(filePath)));
    var documentTree = document.GetSyntaxTree();
    var compilation = project.GetCompilation();
    var compilationUnit = (CompilationUnitSyntax)documentTree.GetRoot();
    var semanticModel = compilation.GetSemanticModel(documentTree);
    var sourceVisitor = new SourceVisitor(semanticModel, entityClass, extensionClass);
    sourceVisitor.Visit(compilationUnit);
    foreach(var m in sourceVisitor.DependenceMethods)
    {
#>
<#=m.Key#> dependent methods
<#+
        foreach (var dm in m.Value.Distinct())
        {
#>
    <#=dm#>
<#+
        }
    }
    foreach(var m in sourceVisitor.DependenceProperties)
    {
#>
<#=m.Key#> dependent properties
<#+
        foreach (var dp in m.Value.Distinct())
        {
#>
    <#=dp#>
<#+
        }
    }
}

class SourceVisitor : SyntaxVisitor
{
    private ISemanticModel _semanticModel;
    private string _entityClass;
    private string _extensionClass;
    private bool _stop;
    private MethodSymbol _method;

    public SourceVisitor(ISemanticModel semanticModel, string entityClass, string extensionClass)
    {
        _semanticModel = semanticModel;
        _entityClass = entityClass;
        _extensionClass = extensionClass;
        DependenceMethods = new Dictionary<MethodSymbol, List<MethodSymbol>>();
        DependenceProperties = new Dictionary<MethodSymbol, List<PropertySymbol>>();
    }

    public override void Visit(SyntaxNode node)
    {
        base.Visit(node);
        if (! _stop)
            foreach (var childNode in node.ChildNodes())
                Visit(childNode);
    }

    public override void VisitClassDeclaration(ClassDeclarationSyntax node)
    {
_stop = node.Identifier.ValueText != _extensionClass;
    }

    public override void VisitMethodDeclaration(MethodDeclarationSyntax node)
    {
        _method = (MethodSymbol)_semanticModel.GetDeclaredSymbol(node);
    }

    public override void VisitInvocationExpression ( InvocationExpressionSyntax node )
    {
        var methodSymbol = (MethodSymbol)_semanticModel.GetSymbolInfo(node).Symbol;
        if (methodSymbol.ContainingType.Name == _entityClass)
        {
            List<MethodSymbol> methods;
            if (! DependenceMethods.TryGetValue(_method, out methods))
                DependenceMethods.Add(_method, methods = new List<MethodSymbol>());
            methods.Add(methodSymbol);
        }
    }

    public override  void VisitMemberAccessExpression ( MemberAccessExpressionSyntax node )
    {
        var propertySymbol = _semanticModel.GetSymbolInfo(node).Symbol as PropertySymbol;
        if (propertySymbol != null && propertySymbol.ContainingType.Name == _entityClass)
        {
            List<PropertySymbol> properties;
            if (! DependenceProperties.TryGetValue(_method, out properties))
                DependenceProperties.Add(_method, properties = new List<PropertySymbol>());
            properties.Add(propertySymbol);
        }
    }

    public Dictionary<MethodSymbol, List<MethodSymbol>> DependenceMethods { get; private set; }
    public Dictionary<MethodSymbol, List<PropertySymbol>> DependenceProperties { get; private set; }
}
#>

Then you just have to call the method Analyze in your T4.

 

Exercise 3:

The goal of the third was to generate a new class that includes validation based on metadata from some code metadata.

From this code:

public static class MetadataDefinition
{
    public static void DefineMaxLength<T>(Func<T, string> getProperty, int maxLength)
    {
    }
    public static void DefineMaxLength<T>(Func<T, string> getProperty, Func<T, int?> getMaxLength)
    {
    }
    public static void DefinePattern<T>(Func<T, string> getProperty, string pattern)
    {
    }
    public static void DefinePattern<T>(Func<T, string> getProperty, Func<T, string> getPattern)
    {
    }
}

public class Address
{
    public string AddressLine { get; set; }
    public string PostalCode { get; set; }
    public string City { get; set; }
    public string Country { get; set; }
}

public static class Specifications
{
    public static void DefineMetadata()
    {
        MetadataDefinition.DefineMaxLength<Address>(a => a.City, 100);
        MetadataDefinition.DefineMaxLength<Address>(a => a.PostalCode,
            a => a.Country != null && a.Country.ToUpper() == "FRANCE" ? 5 : 15);
        MetadataDefinition.DefinePattern<Address>(a => a.PostalCode,
            a => a.Country != null && a.Country.ToUpper() == "FRANCE" ? @"^(\d{2}|(2(A|B)))\d{3}$" : null);
    }
}

we wanted to generate this one:

public class AddressWithValidation
{
    public string AddressLine
    {
        get;
        set;
    }

    private string _PostalCode;
    public string PostalCode
    {
        get
        {
            return _PostalCode;
        }
        set
        {
            ValidatePostalCodeMaxLength(value);
            ValidatePostalCodePattern(value);
            _PostalCode = value;
        }
    }

    private string _City;
    public string City
    {
        get
        {
            return _City;
        }

        set
        {
            ValidateCityMaxLength(value);
            _City = value;
        }
    }

    private string _Country;
    public string Country
    {
        get
        {
            return _Country;
        }

        set
        {
            _Country = value;
            ValidatePostalCodeMaxLength(PostalCode);
            ValidatePostalCodePattern(PostalCode);
        }
    }

    private void ValidatePostalCodeMaxLength(string value)
    {
        if (value == null)
            return;
        int ? maxLength = this.Country != null && this.Country.ToUpper() == "FRANCE" ? 5 : 15;
        if (maxLength.HasValue && value.Length > maxLength)
            throw new InvalidOperationException();
    }

    private void ValidatePostalCodePattern(string value)
    {
        if (value == null)
            return;
        string pattern = this.Country != null && this.Country.ToUpper() == "FRANCE" ? @"^(\d{2}|(2(A|B)))\d{3}$" : null;
        if (pattern != null && !Regex.IsMatch(value, pattern))
            throw new InvalidOperationException();
    }

    private void ValidateCityMaxLength(string value)
    {
        if (value == null)
            return;
        if (value.Length > 100)
            throw new InvalidOperationException();
    }
}

For this, I used the following T4 file:

<#@ template debug="false" hostspecific="true" language="C#" #>
<#@ output extension=".cs" #>
<#@ assembly name="System.Core" #>
<#@ assembly name="Roslyn.Compilers"#>
<#@ assembly name="Roslyn.Compilers.CSharp"#>
<#@ assembly name="Roslyn.Services"#>
<#@ assembly name="Roslyn.Services.CSharp"#>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="Roslyn.Compilers.Common"#>
<#@ import namespace="Roslyn.Compilers.CSharp"#>
<#@ import namespace="Roslyn.Services"#>
<#+
void WriteValidations(string slnPath, string csProjPath, string filePath, string entityClassName,
string specificationsClassName)
{
    var solution = Solution.Load(Path.GetFullPath(Host.ResolvePath(slnPath)));
    var project = solution.Projects.FirstOrDefault(p => p.FilePath == Path.GetFullPath(Host.ResolvePath(csProjPath)));
    var document = project.Documents.First(d => d.FilePath == Path.GetFullPath(Host.ResolvePath(filePath)));
    var documentTree = document.GetSyntaxTree();
    var compilation = project.GetCompilation();
    var compilationUnit = (CompilationUnitSyntax)documentTree.GetRoot();
    var semanticModel = compilation.GetSemanticModel(documentTree);
    var validationMetadataVisitor = new ValidationMetadataVisitor(semanticModel, entityClassName, specificationsClassName);
    validationMetadataVisitor.Visit(compilationUnit);
#>
<#=new ValidationMetadataRewriter(entityClassName, validationMetadataVisitor.ValidationMetadata, semanticModel)
.Visit(compilationUnit).NormalizeWhitespace().ToString()#>
<#+
}



class ValidationMetadataVisitor : SyntaxVisitor
{
    private ISemanticModel _semanticModel;
    private string _entityClassName;
    private string _specificationClassName;
    private bool _stop;
    private List<string> _dependentProperties;

    public override void Visit(SyntaxNode node)
    {
        base.Visit(node);
        if (! _stop)
            foreach (var childNode in node.ChildNodes())
                Visit(childNode);
    }

    public ValidationMetadataVisitor(ISemanticModel semanticModel, string entityClassName, string specificationClassName)
    {
        _semanticModel = semanticModel;
        _entityClassName = entityClassName;
        _specificationClassName = specificationClassName;
        ValidationMetadata = new List<ValidationMetadata>();
    }

    public List<ValidationMetadata> ValidationMetadata { get; private set; }

    public override void VisitClassDeclaration(ClassDeclarationSyntax node)
    {
        _stop = node.Identifier.ValueText != _specificationClassName;
        base.VisitClassDeclaration(node);
    }

    public override void VisitMethodDeclaration(MethodDeclarationSyntax node)
    {
        foreach (var statement in node.Body.Statements)
        {
         var invocation = (InvocationExpressionSyntax)statement.ChildNodes().Single();
        
var invocationMethod = (MethodSymbol)_semanticModel.GetSymbolInfo(invocation).Symbol;
_dependentProperties = new List<string>();
var type = invocationMethod.TypeArguments.Single();
if (type.Name == _entityClassName)
{
Visit(statement);
                var property = ((MemberAccessExpressionSyntax)((SimpleLambdaExpressionSyntax)invocation.ArgumentList.
Arguments[0].Expression).Body).Name.Identifier.ValueText;
                ValidationMetadata.Add(new ValidationMetadata {
MethodName = invocationMethod.Name,
Property = property,
DependentProperties = _dependentProperties.Where(dp => dp != property).ToList(),
ConstantExpression = invocation.ArgumentList.Arguments[1].Expression
as LiteralExpressionSyntax,
LambdaExpression = invocation.ArgumentList.Arguments[1].Expression
as SimpleLambdaExpressionSyntax });
            }
        }
    }


    public override void VisitMemberAccessExpression ( MemberAccessExpressionSyntax node )
    {
        var propertySymbol = _semanticModel.GetSymbolInfo(node).Symbol as PropertySymbol;
        if (propertySymbol != null && propertySymbol.ContainingType.Name == _entityClassName)
            _dependentProperties.Add(propertySymbol.Name);
    }
}

public
 class ValidationMetadata
{
    public string MethodName { get; set; }
    public string Property { get; set; }
    public IEnumerable<string> DependentProperties { get; set; }
    public SimpleLambdaExpressionSyntax LambdaExpression { get; set; }
    public LiteralExpressionSyntax ConstantExpression { get; set; }
}

public class ValidationMetadataRewriter : SyntaxRewriter
{
    private string _entityClassName;
    private List<ValidationMetadata> _validationMetadata;
    private ISemanticModel _semanticModel;

    public ValidationMetadataRewriter(string entityClassName, List<ValidationMetadata> validationMetadata,
ISemanticModel semanticModel)
    {
        _entityClassName = entityClassName;
        _validationMetadata = validationMetadata;
        _semanticModel = semanticModel;
    }

    public override SyntaxNode VisitClassDeclaration(ClassDeclarationSyntax node)
    {
        if (node.Identifier.ValueText != _entityClassName)
            return null;

        var members = node.Members.ToList();
        foreach (var m in node.Members)
        {
            var prop = m as PropertyDeclarationSyntax;
            if (prop != null)
            {
                var properties = _validationMetadata.Where(p => p.Property == prop.Identifier.ValueText).ToList();
                var dependentProperties = _validationMetadata.Where(p => p.DependentProperties.Any(
dp => dp == prop.Identifier.ValueText)).ToList();
                if (properties.Count != 0 || dependentProperties.Count != 0)
                {
                    int index = members.IndexOf(prop);
                    members.RemoveAt(index);
                    members.Insert(index, Syntax.FieldDeclaration(
                        Syntax.VariableDeclaration(prop.Type)
                            .AddVariables(
                                Syntax.VariableDeclarator("_" + prop.Identifier.ValueText)))
                        .WithModifiers(
                            Syntax.TokenList(
                                Syntax.Token(SyntaxKind.PrivateKeyword))));
                    members.Insert(index + 1,
                        Syntax.PropertyDeclaration(prop.Type, prop.Identifier.ValueText)
                            .WithModifiers(prop.Modifiers)
                            .WithAccessorList(
                                Syntax.AccessorList(
                                    Syntax.List<AccessorDeclarationSyntax>(
                                        Syntax.AccessorDeclaration(SyntaxKind.GetAccessorDeclaration,
                                            Syntax.Block(
                                                Syntax.ParseStatement(string.Concat("return _",
prop.Identifier.ValueText,
";")))),
                                        Syntax.AccessorDeclaration(SyntaxKind.SetAccessorDeclaration,
                                            Syntax.Block(
                                                properties.Select(p => Syntax.ParseStatement(string.Concat("Validate",
p.Property, p.MethodName.Substring(6),
"(value);")))
                                                .Union(new [] { Syntax.ParseStatement(string.Concat("_",
prop.Identifier.ValueText,
" = value;")) })
                                                .Union(dependentProperties.Select(dp => Syntax.ParseStatement(
string.Concat("Validate", dp.Property, dp.MethodName.Substring(6),
"(", dp.Property, ");"))))))))));
                    foreach (var p in properties)
                    {
                        var methodBodyStatements = new List<StatementSyntax>();
                        methodBodyStatements.Add(Syntax.ParseStatement("if (value == null) return;"));
                        if (p.LambdaExpression != null)
                            methodBodyStatements.Add(Syntax.LocalDeclarationStatement(
                                Syntax.VariableDeclaration(
                                    Syntax.ParseTypeName(((MethodSymbol)_semanticModel.GetSymbolInfo(p.LambdaExpression)
.Symbol).ReturnType.ToString()))
                                    .WithVariables(
                                        Syntax.SeparatedList<VariableDeclaratorSyntax>(
                                            Syntax.VariableDeclarator(p.MethodName.Substring(6, 1).ToLower() +
p.MethodName.Substring(7))
                                                .WithInitializer(
                                                    Syntax.EqualsValueClause(
                                                        (ExpressionSyntax)new ReplaceParameterByThisRewriter(
p.LambdaExpression.Parameter.Identifier.ValueText)
.Visit(p.LambdaExpression.Body)))))));
                        switch (p.MethodName)
                        {
                            case "DefineMaxLength":
                                methodBodyStatements.Add(Syntax.ParseStatement((p.LambdaExpression == null ?
string.Concat("if (value.Length > ", p.ConstantExpression.NormalizeWhitespace()
.ToString(),
")") : "if (maxLength.HasValue && value.Length > maxLength)") +
"throw new InvalidOperationException();"));
                                break;
                            case "DefinePattern":
                                methodBodyStatements.Add(Syntax.ParseStatement(string.Concat("if (",
p.LambdaExpression ==
null ? "" : "pattern != null && ", "! Regex.IsMatch(value, ",
p.LambdaExpression ==
null ? p.ConstantExpression.NormalizeWhitespace().ToString()
:
"pattern", "))throw new InvalidOperationException();")));
                                break;
                        }
                        members.Add(
                            Syntax.MethodDeclaration(Syntax.ParseTypeName("void"), string.Concat("Validate",
p.Property, p.MethodName.Substring(6)))
                                .WithModifiers(
                                    Syntax.TokenList(
                                        Syntax.Token(SyntaxKind.PrivateKeyword)))
                                .WithParameterList(
                                    Syntax.ParameterList(
                                        Syntax.SeparatedList<ParameterSyntax>(
                                            Syntax.Parameter(
                                                Syntax.Identifier("value"))
                                                .WithType(prop.Type))))
                                .WithBody(
                                    Syntax.Block(methodBodyStatements)));
                    }
                }
            }
        }
        return node.WithIdentifier(Syntax.Identifier(node.Identifier.ValueText + "WithValidation"))
.WithMembers(Syntax.List<MemberDeclarationSyntax>(members));
    }
}

 
public class ReplaceParameterByThisRewriter : SyntaxRewriter
{
    private string _parameterName;

    public ReplaceParameterByThisRewriter(string parameterName)
    {
        _parameterName = parameterName;
    }

    public override SyntaxNode VisitIdentifierName(IdentifierNameSyntax node)
    {
        if (node.Identifier.ValueText == _parameterName)
            return Syntax.ThisExpression ();
        return base.VisitIdentifierName(node);
    }
}
#>

 

Exercise 4:

The goal of the fourth exercise was to update the current document classes in order to add a foo parameter on constructors (or to create a constructor is the class does not have) using NuGet.

For this, I created a ConsoleApplication with the following code:

using Roslyn.Compilers.CSharp;
using System.Collections.Generic;
using System.IO;
using System.Linq;


namespace SELA.NuGet
{
    class Program
    {
        static void Main(string[] args)
        {
            var filePath = args[0];
            string fileCode;
            using (var sr = new StreamReader(filePath))
            {
                fileCode = sr.ReadToEnd();
            }
            var fileSyntax = Syntax.ParseCompilationUnit(fileCode);
            fileCode = new ConstructorAddFooParamater().Visit(fileSyntax).NormalizeWhitespace().ToString();
            using (var sw = new StreamWriter(filePath))
            {
                sw.Write(fileCode);
            }
        }
    }

    class ConstructorAddFooParamater : SyntaxRewriter
    {
        public override SyntaxNode VisitClassDeclaration(ClassDeclarationSyntax node)
        {
            node = (ClassDeclarationSyntax)base.VisitClassDeclaration(node);
            var additionalMembers = new List<MemberDeclarationSyntax>();
            additionalMembers.Add(Syntax.FieldDeclaration(Syntax.VariableDeclaration(Syntax.ParseTypeName("string"))
.WithVariables(
Syntax.SeparatedList(Syntax.VariableDeclarator("_foo"))))
.WithModifiers(
Syntax.TokenList(Syntax.Token(SyntaxKind.PrivateKeyword))));
            if (!node.Members.OfType<ConstructorDeclarationSyntax>().Any())
                additionalMembers.Add((ConstructorDeclarationSyntax)VisitConstructorDeclaration(
Syntax.ConstructorDeclaration(node.Identifier.ValueText)
.WithModifiers(
Syntax.TokenList(Syntax.Token(SyntaxKind.PublicKeyword)))));
            return node.WithMembers(Syntax.List<MemberDeclarationSyntax>(additionalMembers.Union(node.Members)));
        }

        public override SyntaxNode VisitConstructorDeclaration(ConstructorDeclarationSyntax node)
        {
            var parameters = node.ParameterList.Parameters.ToList();
            parameters.Add(Syntax.Parameter(Syntax.Identifier("foo")).WithType(Syntax.ParseTypeName("string")));
            return node.WithParameterList(Syntax.ParameterList(Syntax.SeparatedList(parameters, parameters.Skip(1)
.Select(p =>
Syntax.Token(SyntaxKind.CommaToken))))).WithBody(Syntax.Block((node.Body == null ?
Enumerable.Empty<StatementSyntax>() : node.Body.Statements).Union(new[] {
Syntax.ParseStatement("_foo = foo;") })));
        }
    }
}

Then, I compiled my application.

Then, using NuGetPackageExplorer, I created a new NuGet, then I add the Tools folder. In it I add the following init.ps1 file:

param($installPath, $toolsPath, $package)

ImportModule (JoinPath $toolsPath Sela.psm1)

 

Then I add a new file Sela.psm1 into the Tools folder:

function GetToolsPath()

{

    return [System.IO.Path]::GetDirectoryName((GetModule Sela | select property path)[-1].Path)

}

function SelaSample()

{

    $exePath = JoinPath (GetToolsPath) SELA.NuGet.exe

    $exeArgs = @(‘"’ + $DTE.ActiveDocument.FullName + ‘"’)

    startprocess filepath $exePath ArgumentList $exeArgs Wait

}

ExportModuleMember SelaSample

 

And I also copy the SELA.NuGet.exe into the Tools folder.

After doing it and installing the NuGet package, I can use the SelaSample command on the Package Manager Console in VS to change the current file as expected.

This entry was posted in 10511, 16402, 17894, 17895, 7671. Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *


*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>