90

Typed? Dynamic? Both! Cross-platform DSLs in C#

Embed Size (px)

Citation preview

Page 1: Typed? Dynamic? Both! Cross-platform DSLs in C#
Page 2: Typed? Dynamic? Both! Cross-platform DSLs in C#
Page 3: Typed? Dynamic? Both! Cross-platform DSLs in C#
Page 4: Typed? Dynamic? Both! Cross-platform DSLs in C#
Page 5: Typed? Dynamic? Both! Cross-platform DSLs in C#
Page 6: Typed? Dynamic? Both! Cross-platform DSLs in C#

-And look what you've done to mother! She's worn out fiddling with all your proxy classes. - There's nowt wrong wi' proxy classes, lad! I've generated more proxy classes than you've had hot dinners!

Page 7: Typed? Dynamic? Both! Cross-platform DSLs in C#
Page 8: Typed? Dynamic? Both! Cross-platform DSLs in C#
Page 9: Typed? Dynamic? Both! Cross-platform DSLs in C#

dynamic client = WeirdWildStuffFactory.GiveMeOneOfThose();

client.NowICanPretendAnySillySentenceIsAMethodCall();client.AndICanSendAnyArguments(1, "2", new Stream[] {});

var result = client.MeaningOfLife * 42 * Guid.NewGuid();

Assert.AreEqual(42, result);

Page 10: Typed? Dynamic? Both! Cross-platform DSLs in C#

// Without dynamic binding

((Excel.Range)excelApp.Cells[1,1]).Value2 = "Name";var range2008 = (Excel.Range)excelApp.Cells[1,1];

// With dynamic binding

excelApp.Cells[1,1].Value = "Name";var range2010 = excelApp.Cells[1,1];

Page 11: Typed? Dynamic? Both! Cross-platform DSLs in C#

// With Entity Framework

public User FindUserByEmail(string email){

return _context.Users.Where(x => x.Email == email).FirstOrDefault();

}

// With Simple.Data

public User FindUserByEmail(string email){

return Database.Open().Users.FindAllByEmail(email).FirstOrDefault();

}

Page 12: Typed? Dynamic? Both! Cross-platform DSLs in C#

Background

Given

Name Birth date Height

John 1940-10-09 1.80

Paul 1942-06-18 1.80

George 1943-02-25 1.77

Ringo 1940-07-07 1.68

[Given(@"the following users exist in the database:")]

public void GivenTheFollowingUsersExist(Table table)

{

IEnumerable<dynamic> users = table.CreateDynamicSet();

db.Users.Insert(users);}

Page 13: Typed? Dynamic? Both! Cross-platform DSLs in C#
Page 14: Typed? Dynamic? Both! Cross-platform DSLs in C#

• This talk is not about making a choice on your behalf

Page 15: Typed? Dynamic? Both! Cross-platform DSLs in C#

• In fact, this talk is about leaving you multiple choices

Page 16: Typed? Dynamic? Both! Cross-platform DSLs in C#

• Moreover, this talk is about giving you an opportunity to change your mind later with minimum effort

Page 17: Typed? Dynamic? Both! Cross-platform DSLs in C#

• This talk is about creating libraries that would expose both typed and dynamic API – but not as two different APIs

Page 18: Typed? Dynamic? Both! Cross-platform DSLs in C#

• We will show how to make a single API that can be called from either static typed or dynamic client

Page 19: Typed? Dynamic? Both! Cross-platform DSLs in C#

• Our hybrid API will have hybrid packaging – it will disable dynamic support when installed on platforms that lack runtime method binding

Page 20: Typed? Dynamic? Both! Cross-platform DSLs in C#
Page 21: Typed? Dynamic? Both! Cross-platform DSLs in C#

dynamic results = db.Companies.FindAllByCountry("Norway").FirstOrDefault(); // EXCEPTION

Page 22: Typed? Dynamic? Both! Cross-platform DSLs in C#

var result = db.Companies.Where(x => x.CompanyName == "DynamicSoft").Select( x =>

new{

c.CompanyName,c.YearEstablished

});

Page 23: Typed? Dynamic? Both! Cross-platform DSLs in C#

var result = db.Companies.Where(x => x.CompanyName == "DynamicSoft").Select( x =>

new{

c.CompanyName,c.YearEstablished

});

var result = db.Companies.FindByCompanyName("DynamicSoft").SelectCompanyNameAndYearEstablished();

Page 24: Typed? Dynamic? Both! Cross-platform DSLs in C#

var result = db.Companies.Where(x => x.CompanyName == "DynamicSoft").Select( x =>

new{

c.CompanyName,c.YearEstablished

});

dynamic x = new DynamicQueryExpression();var result = db.Companies

.Where(x.CompanyName == "DynamicSoft")

.Select(x.CompanyName, x.YearEstablished);

Page 25: Typed? Dynamic? Both! Cross-platform DSLs in C#

• My API is my castle, any naming scheme is a matter of a personal preference

Page 26: Typed? Dynamic? Both! Cross-platform DSLs in C#

• So we will leave method naming to personal opinion and ask a different question

Page 27: Typed? Dynamic? Both! Cross-platform DSLs in C#

• Do you want to publish one API or two APIs?

Page 28: Typed? Dynamic? Both! Cross-platform DSLs in C#
Page 29: Typed? Dynamic? Both! Cross-platform DSLs in C#

interface IQueryBuilder{

From(...);Where(...);OrderBy(...);OrderByDescending(...);Select(...);

}

• This interface defines core operation set of our internal DSL and is shared by static typed and dynamic clients

• Typed and dynamic clients differ in what kind of arguments they send to the operations: static typed or dynamic respectively

Page 30: Typed? Dynamic? Both! Cross-platform DSLs in C#

var result = db.Companies.Where(x => x.CompanyName > "D").OrderBy(x => x.CompanyName);

dynamic x = new DynamicQueryExpression();var result = db.Companies

.Where(x.CompanyName > "D")

.OrderBy(x.CompanyName);

• Exposing single API with two similar looking parameter syntax flavors unifies API operations across paradigms

• API users learn the same API operations no matter what paradigm they choose

• API users can switch paradigms at relatively low costs

Page 31: Typed? Dynamic? Both! Cross-platform DSLs in C#
Page 32: Typed? Dynamic? Both! Cross-platform DSLs in C#

LINQ expressions

custom expression

LINQ expressioncustom expression

custom expressioncustom expression

IDynamicMetaObjectProvider

Page 33: Typed? Dynamic? Both! Cross-platform DSLs in C#

public class DynamicQueryExpression : QueryExpression, IDynamicMetaObjectProvider {

public DynamicQueryExpression() {}}

public interface IFinder{

Result Find<T>(Expression<Func<T>,bool>> query);Result Find(QueryExpression query);

}

public class QueryExpression{

// no public constructor}

Finder.Dynamic.dll

Finder.dll

Page 34: Typed? Dynamic? Both! Cross-platform DSLs in C#

dynamic x = new DynamicQueryExpression();var finder = new Finder();var result = finder.Find(x.Companies).Where(

x.CompanyName.StartsWith("D") &&x.YearEstablished > 2000);

var finder = new Finder();var result = finder.Find<Companies>().Where(x =>

x.CompanyName.StartsWith("D") &&x.YearEstablished > 2000);

Dynamic client

Static typed client

Page 35: Typed? Dynamic? Both! Cross-platform DSLs in C#
Page 36: Typed? Dynamic? Both! Cross-platform DSLs in C#
Page 37: Typed? Dynamic? Both! Cross-platform DSLs in C#

var result1 = from c in db.Companieswhere c.CompanyName.StartsWith("D")orderby c.CompanyNameselect new

{c.CompanyName,c.YearEstablished

};

var result2 = db.Companies.Where(x => x.CompanyName.StartsWith("D").OrderBy(x => x.CompanyName).Select( x =>

new{

c.CompanyName,c.YearEstablished

});

Page 38: Typed? Dynamic? Both! Cross-platform DSLs in C#

interface ICommandBuilder{

From(...)Where(...)OrderBy(...)OrderByDescending(...)Select(...)

}

Page 39: Typed? Dynamic? Both! Cross-platform DSLs in C#

interface ICommandBuilder{

ICommandBuilder From(string tableName);ICommandBuilder Where(string condition);ICommandBuilder OrderBy(params string[] columns);ICommandBuilder OrderByDescending(params string[] columns);ICommandBuilder Select(params string[] columns);

Command Build();}

// Usage example

var command = new CommandBuilder().From("Companies").Where("YearEstablished>2000 AND NumberOfEmployees<100").OrderBy("Country").Select("CompanyName", "Country", "City").Build();

Page 40: Typed? Dynamic? Both! Cross-platform DSLs in C#

public class Command{

private string _table;private string _where;private List<string> _selectColumns;private List<KeyValuePair<string, bool>> _orderByColumns;

...}

Page 41: Typed? Dynamic? Both! Cross-platform DSLs in C#

private string Format(){

var builder = new StringBuilder();

builder.AppendFormat("SELECT {0} FROM {1}",_selectColumns.Any() ? string.Join(",", _selectColumns) : "*", _table);

if (!string.IsNullOrEmpty(_where))builder.AppendFormat(" WHERE {0}", _where);

if (_orderByColumns.Any()) {builder.AppendFormat(" ORDER BY {0}",

string.Join(",", _orderByColumns.Select(x => x.Key + (x.Value ? " DESC" : string.Empty))));

}

return builder.ToString();}

Page 42: Typed? Dynamic? Both! Cross-platform DSLs in C#

interface ICommandBuilder{

ICommandBuilder<T> From<T>();}

interface ICommandBuilder<T>{

ICommandBuilder<T> Where(Expression<Func<T, bool>> expression);

ICommandBuilder<T> OrderBy(Expression<Func<T, object>> expression);

ICommandBuilder<T> OrderByDescending(Expression<Func<T, object>> expression);

ICommandBuilder<T> Select(Expression<Func<T, object>> expression);

Command Build();}

Page 43: Typed? Dynamic? Both! Cross-platform DSLs in C#

var command = new CommandBuilder().From<Companies>().Where(x =>

x.YearEstablished > 2000 &&x.NumberOfEmployees < 100)

.OrderBy(x =>x.Country)

.Select(x => new { x.CompanyName, x.Country, x.City })

.Build();

Page 44: Typed? Dynamic? Both! Cross-platform DSLs in C#
Page 45: Typed? Dynamic? Both! Cross-platform DSLs in C#

Nobody expects to parse LINQ expression trees!

Page 46: Typed? Dynamic? Both! Cross-platform DSLs in C#
Page 47: Typed? Dynamic? Both! Cross-platform DSLs in C#

var command = new CommandBuilder().From<Companies>().Where(x =>

x.YearEstablished > 2000 &&x.NumberOfEmployees < 100)

.OrderBy(x =>x.Country)

.Select(x => new { x.CompanyName, x.Country, x.City })

.Build();

Page 48: Typed? Dynamic? Both! Cross-platform DSLs in C#

Expression<Func<Companies, bool>> expression = x => x.YearEstablished > 2000 && x.NumberOfEmployees < 100;

Lambda

AndAlso

GreaterThan

MemberAccess Constant

2000

Constant

100

MemberAccess

LessThan

Body

YearEstablished

NumberOfEmployees

Left Right

Left Right RightLeft

Member Value Member Value

Int32 Int32

Page 49: Typed? Dynamic? Both! Cross-platform DSLs in C#
Page 50: Typed? Dynamic? Both! Cross-platform DSLs in C#
Page 51: Typed? Dynamic? Both! Cross-platform DSLs in C#

builder.From<Table>()

command.Where(x => expression<Func>T, bool>>

CommandExpression.FromLinqExpression(expression.Body)

builder.Build()

Assigns the type

Assigns Where expression

Converts tocustom expression

Evaluatesthe expression

Page 52: Typed? Dynamic? Both! Cross-platform DSLs in C#

• Defined ICommandBuilder and ICommandBuilder<T> interfaces

• Implemented CommandBuilder

• Implemented CommandExpression• LINQ expression parsing

• Expression evaluation

Page 53: Typed? Dynamic? Both! Cross-platform DSLs in C#

interface ICommandBuilder{

ICommandBuilder<T> From<T>();}

interface ICommandBuilder<T>{

ICommandBuilder<T> Where(Expression<Func<T, bool>> expression);

ICommandBuilder<T> OrderBy(Expression<Func<T, object>> expression);

ICommandBuilder<T> OrderByDescending(Expression<Func<T, object>> expression);

ICommandBuilder<T> Select(Expression<Func<T, object>> expression);

Command Build();}

Page 54: Typed? Dynamic? Both! Cross-platform DSLs in C#

var result = db.From<Companies>().Where(x => x.CompanyName == "DynamicSoft").Select( x =>

new{

c.CompanyName,c.YearEstablished

});

dynamic x = new DynamicQueryExpression();var result = db.From(x.Companies)

.Where(x.CompanyName == "DynamicSoft")

.Select(x.CompanyName, x.YearEstablished);

Page 55: Typed? Dynamic? Both! Cross-platform DSLs in C#

var result = db.From<Companies>().Where(x => x.CompanyName == "DynamicSoft").Select( x =>

new{

c.CompanyName,c.YearEstablished

});

dynamic x = new DynamicQueryExpression();var result = db.From(x.Companies)

.Where(x.CompanyName == "DynamicSoft")

.Select(x.CompanyName, x.YearEstablished);

Page 56: Typed? Dynamic? Both! Cross-platform DSLs in C#

• Core API is static typed and packaged in assembly that doesn’t reference types from System.Dynamic namespace

• API exposes a DSL based on LINQ expressions

custom expression

LINQ expressioncustom expression

custom expressioncustom expression

IDynamicMetaObjectProvider

• Dynamic extensions for the API are packaged in a different assembly that is deployed on platforms with DLR support

Page 57: Typed? Dynamic? Both! Cross-platform DSLs in C#

interface ICommandBuilder{

ICommandBuilder<T> From<T>();ICommandBuilder<object> From<T>(

CommandExpression expression);}

Page 58: Typed? Dynamic? Both! Cross-platform DSLs in C#

interface ICommandBuilder<T>{

ICommandBuilder<T> Where(Expression<Func<T, bool>> expression);

ICommandBuilder<T> Where(CommandExpression expression);ICommandBuilder<T> OrderBy(

Expression<Func<T, object>> expression);ICommandBuilder<T> OrderBy(

params CommandExpression[] columns);ICommandBuilder<T> OrderByDescending(

Expression<Func<T, object>> expression);ICommandBuilder<T> OrderByDescending(

params CommandExpression[] columns);ICommandBuilder<T> Select(

Expression<Func<T, object>> expression);ICommandBuilder<T> Select(

params CommandExpression[] columns);Command Build();

}

Page 59: Typed? Dynamic? Both! Cross-platform DSLs in C#

interface ICommandBuilder<T>{

...

ICommandBuilder<T> Where(Expression<Func<T, bool>> expression);

ICommandBuilder<T> Where(CommandExpression expression);

...}

// Typed clientbuilder.Where(x => x.CompanyName == "DynamicSoft");

// Dynamic clientx = new DynamicCommandExpression()builder.Where(x.DynamicName == "DynamicSoft");

Page 60: Typed? Dynamic? Both! Cross-platform DSLs in C#

interface ICommandBuilder<T>{

...

ICommandBuilder<T> OrderBy(Expression<Func<T, object>> expression);

ICommandBuilder<T> OrderBy(params CommandExpression[] columns);

...}

// Typed clientbuilder.OrderBy(x => x.CompanyName);

// Dynamic clientx = new DynamicCommandExpression()builder.OrderBy(x.CompanyName);

Page 61: Typed? Dynamic? Both! Cross-platform DSLs in C#

interface ICommandBuilder<T>{

...

ICommandBuilder<T> Select(Expression<Func<T, object>> expression);

ICommandBuilder<T> Select(params CommandExpression[] columns);

...}

// Typed clientbuilder.Select(x => new { x.Country, x.CompanyName });

// Dynamic clientx = new DynamicCommandExpression()builder.Select(x.Country, x.CompanyName);

Page 62: Typed? Dynamic? Both! Cross-platform DSLs in C#

public ICommandBuilder<T> Where(Expression<Func<T, bool>> expression)

{_command.Where(

CommandExpression.FromLinqExpression(expression.Body));return this;

}

public ICommandBuilder<T> Where(CommandExpression expression)

{_command.Where(expression);return this;

}

Page 63: Typed? Dynamic? Both! Cross-platform DSLs in C#

DynamicObject

CommandExpression

IDynamicMetaObjectProviderCommandExpression

DynamicCommandExpression

DynamicMetaObject

• DynamicMetaObject

Page 64: Typed? Dynamic? Both! Cross-platform DSLs in C#
Page 65: Typed? Dynamic? Both! Cross-platform DSLs in C#

Where(dynamic expression)

Where(CommandExpression)

DynamicCommandExpressionto CommandExpression

builder.Where(expression).Build()

Call with dynamicargument

Finds suitablemethod overload

Converts totyped expression

Follow typedbuilder workflow

Page 66: Typed? Dynamic? Both! Cross-platform DSLs in C#

input

return

CommandProcessor

Page 67: Typed? Dynamic? Both! Cross-platform DSLs in C#

// Typed

var command = commandBuilder.From<Companies>().Build();

var commandProcessor = new CommandProcessor(command);var result = commandProcessor.FindOne<Companies>();

// Dynamic

var x = new DynamicCommandExpression();var command = commandBuilder

.From(x.Companies)

.Build();var commandProcessor = new CommandProcessor(command);var row = commandProcessor.FindOne(x.Companies);

Page 68: Typed? Dynamic? Both! Cross-platform DSLs in C#

public interface ICommandProcessor{

T FindOne<T>();IEnumerable<T> FindAll<T>();

}

public abstract class CommandProcessor : ICommandProcessor{

protected readonly Command _command;

protected CommandProcessor(Command command){

_command = command;}

...

protected abstract IEnumerable<IDictionary<string, object>> Execute();

}

Page 69: Typed? Dynamic? Both! Cross-platform DSLs in C#

IDictionary string object

objectIDictionary string object

IEnumerable IDictionary string object

objectIEnumerable IDictionary string object

Page 70: Typed? Dynamic? Both! Cross-platform DSLs in C#

dynamic x = new DynamicCommandExpression();var command = SelectAllCommand();var commandProcessor = new FakeCommandProcessor(command);

// Single row

var result = commandProcessor.FindOne();Assert.AreEqual("DynamicSoft", result["CompanyName"]);

// Collection

var result = commandProcessor.FindAll();Assert.AreEqual(2, result.Count());Assert.AreEqual("DynamicSoft", result.First()["CompanyName"]);Assert.AreEqual("StaticSoft", result.Last()["CompanyName"]);

Page 71: Typed? Dynamic? Both! Cross-platform DSLs in C#

public interface ICommandProcessor{

T FindOne<T>() where T : class;ResultRow FindOne();ResultRow FindOne(CommandExpression expression);IEnumerable<T> FindAll<T>() where T : class;IEnumerable<ResultRow> FindAll();ResultCollection FindAll(CommandExpression expression);

}

Page 72: Typed? Dynamic? Both! Cross-platform DSLs in C#

public class DynamicResultRow : ResultRow, IDynamicMetaObjectProvider

{internal DynamicResultRow(IDictionary<string, object> data)

: base(data){}...

}

public class DynamicResultCollection : ResultCollection, IDynamicMetaObjectProvider

{internal DynamicResultCollection(IEnumerable<ResultRow> data)

: base(data){}...

}

Page 73: Typed? Dynamic? Both! Cross-platform DSLs in C#

dynamic x = new DynamicCommandExpression();var command = SelectAllCommand();var commandProcessor = new FakeCommandProcessor(command);

// Dynamic result// Requires BindGetMember overloadvar result = commandProcessor.FindOne();Assert.AreEqual("DynamicSoft", result.CompanyName);

// Typed result// Requires BindConvert overloadCompanies result = commandProcessor.FindOne();Assert.AreEqual("DynamicSoft", result.CompanyName);

Page 74: Typed? Dynamic? Both! Cross-platform DSLs in C#
Page 75: Typed? Dynamic? Both! Cross-platform DSLs in C#

processor.FindAll<T>(x => expression<T>)

processor.Execute()

IEnumerable<IDictionary<string, object>>

ToObject<T>()

Call to a genericmethod overload

Executes SQL command

Returns nativeresult

Converts resultsto typed objects

Page 76: Typed? Dynamic? Both! Cross-platform DSLs in C#

processor.FindAll(dynamic)

processor.Execute()

ToObject<DynamicResultCollection>()

IEnumerable<T> results

Call to a non-genericmethod overload

Follows typedexecution path

Converts resultsto typed objects

Converts resultsto typed objects

Page 77: Typed? Dynamic? Both! Cross-platform DSLs in C#
Page 78: Typed? Dynamic? Both! Cross-platform DSLs in C#
Page 79: Typed? Dynamic? Both! Cross-platform DSLs in C#
Page 80: Typed? Dynamic? Both! Cross-platform DSLs in C#
Page 81: Typed? Dynamic? Both! Cross-platform DSLs in C#

MAKEHYBRID

APINOT

HYBRID WAR

Page 82: Typed? Dynamic? Both! Cross-platform DSLs in C#
Page 83: Typed? Dynamic? Both! Cross-platform DSLs in C#
Page 84: Typed? Dynamic? Both! Cross-platform DSLs in C#
Page 85: Typed? Dynamic? Both! Cross-platform DSLs in C#
Page 86: Typed? Dynamic? Both! Cross-platform DSLs in C#
Page 87: Typed? Dynamic? Both! Cross-platform DSLs in C#

LINQ expressions

custom expression

LINQ expressioncustom expression

custom expressioncustom expression

IDynamicMetaObjectProvider

Page 88: Typed? Dynamic? Both! Cross-platform DSLs in C#
Page 89: Typed? Dynamic? Both! Cross-platform DSLs in C#
Page 90: Typed? Dynamic? Both! Cross-platform DSLs in C#