Distributed Systems with ZeroMQ and gevent

Jeff Lindsay @progrium

Why distributed systems? Harness more CPUs and resources Run faster in parallel Tolerance of individual failures Better separation of concerns

Most web apps evolve into distributed systems

OpenStack

Amazon AWS Provider Web

API

Provider

Client Provider TwiML

ZeroMQ + gevent Two powerful and misunderstood tools

Concurrency Heart of Distributed Systems

Distributed computing is just another flavor of local concurrency

Multithreading Shared Memory Thread

Thread

Thread

Distributed system Shared Database App

App

App

Concurrency models Execution model Defines the “computational unit” Communication model Means of sharing and coordination

Concurrency models Traditional multithreading OS threads Shared memory, locks, etc Async or Evented I/O I/O loop + callback chains Shared memory, futures Actor model Shared nothing “processes” Built-in messaging

Examples Erlang Actor model Scala Actor model Go Channels, Goroutines Everything else (Ruby, Python, PHP, Perl, C/C++, Java) Threading Evented

Erlang is special. Normally, the networking of distributed systems is tacked on to the local concurrency model. MQ, RPC, REST, ...

Why not always use Erlang?

Why not always use Erlang? Half reasons Weird/ugly language Limited library ecosystem VM requires operational expertise Functional programming isn’t mainstream

Why not always use Erlang? Half reasons Weird/ugly language Limited library ecosystem VM requires operational expertise Functional programming isn’t mainstream Biggest reason It’s not always the right tool for the job

Amazon AWS Provider Web

API

Provider

Client Provider TwiML

Service Oriented Architecture Multiple languages Heterogeneous cluster

RPC

RPC Client / server

RPC Client / server Mapping to functions

RPC Client / server Mapping to functions Message serialization

RPC Client / server Mapping to functions Message serialization Poor abstraction of what you really want

What you want are tools to help you get distributed actor model concurrency like Erlang ... without Erlang. Even better if they're decoupled and optional.

Rarely will you build an application as part of a distributed system that does not also need local concurrency.

Communication model How do we unify communications in local concurrency and distributed systems across languages?

Execution model How do we get Erlang-style local concurrency without interfering with the language's idiomatic paradigm?

ZeroMQ Communication model

Misconceptions

Misconceptions It’s just another MQ, right?

Misconceptions It’s just another MQ, right? Not really.

Misconceptions It’s just another MQ, right? Not really.

Misconceptions It’s just another MQ, right? Not really. Oh, it’s just sockets, right?

Misconceptions It’s just another MQ, right? Not really. Oh, it’s just sockets, right? Not really.

Misconceptions It’s just another MQ, right? Not really. Oh, it’s just sockets, right? Not really.

Misconceptions It’s just another MQ, right? Not really. Oh, it’s just sockets, right? Not really. Wait, isn’t messaging a solved problem?

Misconceptions It’s just another MQ, right? Not really. Oh, it’s just sockets, right? Not really. Wait, isn’t messaging a solved problem? *sigh* ... maybe.

Regular Sockets

Regular Sockets

Point to point

Regular Sockets

Point to point Stream of bytes

Regular Sockets

Point to point Stream of bytes Buffering

Regular Sockets

Point to point Stream of bytes Buffering Standard API

Regular Sockets

Point to point Stream of bytes Buffering Standard API TCP/IP or UDP, IPC

Messaging

Messaging Messages are atomic

Messaging Messages are atomic

Messaging Messages are atomic

Messaging Messages are atomic

Messaging Messages are atomic

Messages can be routed

Messaging Messages are atomic

Messages can be routed

Messaging Messages are atomic

Messages can be routed

Messaging Messages are atomic

Messages may sit around

Messages can be routed

Messaging Messages are atomic

Messages may sit around

Messages can be routed

Messaging Messages are atomic

Messages may sit around

Messages can be routed

Messaging Messages are atomic

Messages may sit around

Messages can be routed

Messaging Messages are atomic

Messages may sit around

Messages can be routed

Messaging Messages are atomic

Messages may sit around

Messages can be routed

Messaging Messages are atomic

Messages can be routed

Messages may sit around

Messages are delivered

Messaging Messages are atomic

Messages can be routed

Messages may sit around

Messages are delivered

Rise of the Big MQ

App App

Reliable Message Broker

Persistent Queues

App

App

App App

App

App

AMQP MQ

Producer

Consumer

AMQP MQ

Producer

X Exchange

Binding Consumer Queue

AMQP MQ

Producer

X Exchange

Consumer Queue

AMQP Recipes

AMQP Recipes Work queues Distributing tasks among workers

AMQP Recipes Work queues

Publish/Subscribe

Distributing tasks among workers

Sending to many consumers at once

X

AMQP Recipes Work queues

Publish/Subscribe

Distributing tasks among workers

Sending to many consumers at once

X

Routing Receiving messages selectively foo X

bar baz

AMQP Recipes Work queues

Publish/Subscribe

Distributing tasks among workers

Sending to many consumers at once

X

Routing

RPC

Receiving messages selectively

Remote procedure call implementation

foo X

bar baz

Drawbacks of Big MQ Lots of complexity Queues are heavyweight HA is a challenge Poor primitives

Enter ZeroMQ “Float like a butterfly, sting like a bee”

Echo in Python Server 1 2 3 4 5 6 7 8 9

import zmq context = zmq.Context() socket = context.socket(zmq.REP) socket.bind("tcp://127.0.0.1:5000") while True: msg = socket.recv() print "Received", msg socket.send(msg)

Client 1 2 3 4 5 6 7 8 9 10

import zmq context = zmq.Context() socket = context.socket(zmq.REQ) socket.connect("tcp://127.0.0.1:5000") for i in range(10): msg = "msg %s" % i socket.send(msg) print "Sending", msg reply = socket.recv()

Echo in Ruby Server 1 2 3 4 5 6 7 8 9 10

require "zmq" context = ZMQ::Context.new(1) socket = context.socket(ZMQ::REP) socket.bind("tcp://127.0.0.1:5000") loop do msg = socket.recv puts "Received #{msg}" socket.send(msg) end

Client 1 2 3 4 5 6 7 8 9 10 11

require "zmq" context = ZMQ::Context.new(1) socket = context.socket(ZMQ::REQ) socket.connect("tcp://127.0.0.1:5000") (0...10).each do |i| msg = "msg #{i}" socket.send(msg) puts "Sending #{msg}" reply = socket.recv end

Echo in PHP Server 1 2 3 4 5 6 7 8 9 10 11

getSocket(ZMQ::SOCKET_REP); $socket->bind("tcp://127.0.0.1:5000"); while (true) { $msg = $socket->recv(); echo "Received {$msg}"; $socket->send($msg); } ?>

Client 1 2 3 4 5 6 7 8 9 10 11 12

getSocket(ZMQ::SOCKET_REQ); $socket->connect("tcp://127.0.0.1:5000"); foreach (range(0, 9) as $i) { $msg = "msg {$i}"; $socket->send($msg); echo "Sending {$msg}"; $reply = $socket->recv(); } ?>

Bindings ActionScript, Ada, Bash, Basic, C, Chicken Scheme, Common Lisp, C#, C++, D, Erlang, F#, Go, Guile, Haskell, Haxe, Java, JavaScript, Lua, Node.js, Objective-C, Objective Caml, ooc, Perl, PHP, Python, Racket, REBOL, Red, Ruby, Smalltalk

Plumbing

Plumbing

Plumbing

Plumbing

Plumbing

Plumbing inproc ipc tcp multicast

Plumbing inproc ipc tcp multicast socket.bind("tcp://localhost:5560") socket.bind("ipc:///tmp/this-socket") socket.connect("tcp://10.0.0.100:9000") socket.connect("ipc:///tmp/another-socket") socket.connect("inproc://another-socket")

Plumbing inproc ipc tcp multicast socket.bind("tcp://localhost:5560") socket.bind("ipc:///tmp/this-socket") socket.connect("tcp://10.0.0.100:9000") socket.connect("ipc:///tmp/another-socket") socket.connect("inproc://another-socket")

Plumbing inproc ipc tcp multicast socket.bind("tcp://localhost:5560") socket.bind("ipc:///tmp/this-socket") socket.connect("tcp://10.0.0.100:9000") socket.connect("ipc:///tmp/another-socket") socket.connect("inproc://another-socket")

Message Patterns

Message Patterns Request-Reply

REQ

REP

Message Patterns Request-Reply REP

REP

REQ REP

Message Patterns Request-Reply REP

REP

REQ REP

Message Patterns Request-Reply REP

REP

REQ REP

Message Patterns Request-Reply REP

REP

REQ REP

Message Patterns Request-Reply

Publish-Subscribe

REP

SUB REP

REQ REP

SUB

PUB SUB

Message Patterns Request-Reply

Publish-Subscribe

REP

SUB REP

REQ

SUB

REP

Push-Pull (Pipelining) PULL PULL

PUSH

PULL

SUB

PUB

Message Patterns Request-Reply

Publish-Subscribe

REP

SUB REP

REQ

SUB

REP

Push-Pull (Pipelining) PULL PULL

PUSH

PULL

SUB

PUB

Message Patterns Request-Reply

Publish-Subscribe

REP

SUB REP

REQ

SUB

REP

Push-Pull (Pipelining) PULL PULL

PUSH

PULL

SUB

PUB

Message Patterns Request-Reply

Publish-Subscribe

REP

SUB REP

REQ

SUB

REP

Push-Pull (Pipelining) PULL PULL

PUSH

PULL

SUB

PUB

Message Patterns Request-Reply

Publish-Subscribe

REP

SUB REP

REQ

SUB

PUB SUB

REP

Push-Pull (Pipelining)

Pair

PULL PULL

PUSH

PULL

PAIR

PAIR

Devices Queue

Forwarder

Streamer

Design architectures around devices.

Devices Queue

Forwarder

REQ

Streamer

REP

Design architectures around devices.

Devices Queue

Forwarder

PUB

Streamer

SUB

Design architectures around devices.

Devices Queue

Forwarder

PUSH

Streamer

PULL

Design architectures around devices.

Performance

Performance Orders of magnitude faster than most MQs

Performance Orders of magnitude faster than most MQs Higher throughput than raw sockets

Performance Orders of magnitude faster than most MQs Higher throughput than raw sockets Intelligent message batching

Performance Orders of magnitude faster than most MQs Higher throughput than raw sockets Intelligent message batching Edge case optimizations

Concurrency? "Come for the messaging, stay for the easy concurrency"

Hintjens’ Law of Concurrency

e=

2 mc

E is effort, the pain that it takes M is mass, the size of the code C is conflict, when C threads collide

Hintjens’ Law of Concurrency

Hintjens’ Law of Concurrency

Hintjens’ Law of Concurrency

ZeroMQ: 2 e=mc ,

for c=1

ZeroMQ Easy ... familiar socket API Cheap ... lightweight queues in a library Fast ... higher throughput than raw TCP Expressive ... maps to your architecture Messaging toolkit for concurrency and distributed systems.

gevent Execution model

Threading vs Evented Evented seems to be preferred for scalable I/O applications

Evented Stack Non-blocking Code Flow Control I/O Abstraction Reactor Event Poller

I/O Loop

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34

def lookup(country, search_term): main_d = defer.Deferred() def first_step(): query = "http://www.google.%s/search?q=%s" % (country,search_term) d = getPage(query) d.addCallback(second_step, country) d.addErrback(failure, country) def second_step(content, country): m = re.search('
http://[^"]+)"', content, re.DOTALL) if not m: main_d.callback(None) return url = m.group('url') d = getPage(url) d.addCallback(third_step, country, url) d.addErrback(failure, country) def third_step(content, country, url): m = re.search("(.*?)", content) if m: title = m.group(1) main_d.callback(dict(url = url, title = title)) else: main_d.callback(dict(url=url, title="{not-specified}")) def failure(e, country): print ".%s FAILED: %s" % (country, str(e)) main_d.callback(None) first_step() return main_d

gevent “Regular” Python Greenlets

Monkey patching

Reactor / Event Poller

Green threads “Threads” implemented in user space (VM, library)

Monkey patching socket, ssl, threading, time

Twisted

Twisted ~400 modules

gevent 25 modules

Performance

http://nichol.as

Performance

http://nichol.as

Performance

http://nichol.as

Building a Networking App 1 2 3 4 5 6 7 8 9 10 11 12 13

#=== # 1. Basic gevent TCP server from gevent.server import StreamServer def handle_tcp(socket, address): print 'new tcp connection!' while True: socket.send('hello\n') gevent.sleep(1) tcp_server = StreamServer(('127.0.0.1', 1234), handle_tcp) tcp_server.serve_forever()

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22

#=== # 2. Basic gevent TCP server and WSGI server from gevent.pywsgi import WSGIServer from gevent.server import StreamServer def handle_http(env, start_response): start_response('200 OK', [('Content-Type', 'text/html')]) print 'new http request!' return ["hello world"] def handle_tcp(socket, address): print 'new tcp connection!' while True: socket.send('hello\n') gevent.sleep(1) tcp_server = StreamServer(('127.0.0.1', 1234), handle_tcp) tcp_server.start() http_server = WSGIServer(('127.0.0.1', 8080), handle_http) http_server.serve_forever()

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31

from gevent.pywsgi import WSGIServer from gevent.server import StreamServer from gevent.socket import create_connection def handle_http(env, start_response): start_response('200 OK', [('Content-Type', 'text/html')]) print 'new http request!' return ["hello world"] def handle_tcp(socket, address): print 'new tcp connection!' while True: socket.send('hello\n') gevent.sleep(1) def client_connect(address): sockfile = create_connection(address).makefile() while True: line = sockfile.readline() # returns None on EOF if line is not None: print "<<<", line, else: break tcp_server = StreamServer(('127.0.0.1', 1234), handle_tcp) tcp_server.start() gevent.spawn(client_connect, ('127.0.0.1', 1234)) http_server = WSGIServer(('127.0.0.1', 8080), handle_http) http_server.serve_forever()

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32

from gevent.pywsgi import WSGIServer from gevent.server import StreamServer from gevent.socket import create_connection def handle_http(env, start_response): start_response('200 OK', [('Content-Type', 'text/html')]) print 'new http request!' return ["hello world"] def handle_tcp(socket, address): print 'new tcp connection!' while True: socket.send('hello\n') gevent.sleep(1) def client_connect(address): sockfile = create_connection(address).makefile() while True: line = sockfile.readline() # returns None on EOF if line is not None: print "<<<", line, else: break tcp_server = StreamServer(('127.0.0.1', 1234), handle_tcp) http_server = WSGIServer(('127.0.0.1', 8080), handle_http) greenlets = [ gevent.spawn(tcp_server.serve_forever), gevent.spawn(http_server.serve_forever), gevent.spawn(client_connect, ('127.0.0.1', 1234)), ] gevent.joinall(greenlets)

ZeroMQ in gevent?

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24

from gevent import spawn from gevent_zeromq import zmq context = zmq.Context() def serve(): socket = context.socket(zmq.REP) socket.bind("tcp://localhost:5559") while True: message = socket.recv() print "Received request: ", message socket.send("World") server = spawn(serve) def client(): socket = context.socket(zmq.REQ) socket.connect("tcp://localhost:5559") for request in range(10): socket.send("Hello") message = socket.recv() print "Received reply ", request, "[", message, "]" spawn(client).join()

Actor model? Easy to implement, in whole or in part, optionally with ZeroMQ

What is gevent missing?

What is gevent missing? Documentation

What is gevent missing? Documentation Application framework

gservice Application framework for gevent

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32

from gevent.pywsgi import WSGIServer from gevent.server import StreamServer from gevent.socket import create_connection def handle_http(env, start_response): start_response('200 OK', [('Content-Type', 'text/html')]) print 'new http request!' return ["hello world"] def handle_tcp(socket, address): print 'new tcp connection!' while True: socket.send('hello\n') gevent.sleep(1) def client_connect(address): sockfile = create_connection(address).makefile() while True: line = sockfile.readline() # returns None on EOF if line is not None: print "<<<", line, else: break tcp_server = StreamServer(('127.0.0.1', 1234), handle_tcp) http_server = WSGIServer(('127.0.0.1', 8080), handle_http) greenlets = [ gevent.spawn(tcp_server.serve_forever), gevent.spawn(http_server.serve_forever), gevent.spawn(client_connect, ('127.0.0.1', 1234)), ] gevent.joinall(greenlets)

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31

from gevent.pywsgi import WSGIServer from gevent.server import StreamServer from gevent.socket import create_connection from gservice.core import Service def handle_http(env, start_response): start_response('200 OK', [('Content-Type', 'text/html')]) print 'new http request!' return ["hello world"] def handle_tcp(socket, address): print 'new tcp connection!' while True: socket.send('hello\n') gevent.sleep(1) def client_connect(address): sockfile = create_connection(address).makefile() while True: line = sockfile.readline() # returns None on EOF if line is not None: print "<<<", line, else: break app = Service() app.add_service(StreamServer(('127.0.0.1', 1234), handle_tcp)) app.add_service(WSGIServer(('127.0.0.1', 8080), handle_http)) app.add_service(TcpClient(('127.0.0.1', 1234), client_connect)) app.serve_forever()

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32

from gservice.core import Service from gservice.config import Setting class MyApplication(Service): http_port = Setting('http_port') tcp_port = Setting('tcp_port') connect_address = Setting('connect_address') def __init__(self): self.add_service(WSGIServer(('127.0.0.1', self.http_port), self.handle_http)) self.add_service(StreamServer(('127.0.0.1', self.tcp_port), self.handle_tcp)) self.add_service(TcpClient(self.connect_address, self.client_connect)) def client_connect(self, address): sockfile = create_connection(address).makefile() while True: line = sockfile.readline() # returns None on EOF if line is not None: print "<<<", line, else: break def handle_tcp(self, socket, address): print 'new tcp connection!' while True: socket.send('hello\n') gevent.sleep(1) def handle_http(self, env, start_response): start_response('200 OK', [('Content-Type', 'text/html')]) print 'new http request!' return ["hello world"]

1 2 3 4 5 6 7 8 9 10 11

# example.conf.py pidfile = 'example.pid' logfile = 'example.log' http_port = 8080 tcp_port = 1234 connect_address = ('127.0.0.1', 1234) def service(): from example import MyApplication return MyApplication() # Run in the foreground gservice -C example.conf.py # Start service as daemon gservice -C example.conf.py start # Control service gservice -C example.conf.py restart gservice -C example.conf.py reload gservice -C example.conf.py stop # Run with overriding configuration gservice -C example.conf.py -X 'http_port = 7070'

Generalizing gevent proves a model that can be implemented in almost any language that can implement an evented stack

gevent Easy ... just normal Python Small ... only 25 modules Fast ... top performing server Compatible ... works with most libraries Futuristic evented platform for network applications.

Raiden Lightning fast, scalable messaging https://github.com/progrium/raiden

Concurrency models Traditional multithreading Async or Evented I/O Actor model

Conclusion

Two very simple, but very powerful tools for distributed / concurrent systems

Thanks @progrium

with ZeroMQ and gevent - GitHub

Normally, the networking of distributed systems is ... Service Oriented .... while True: msg = socket.recv() print "Received", msg socket.send(msg). 1. 2. 3. 4. 5. 6. 7.

3MB Sizes 22 Downloads 234 Views

Recommend Documents

visualization with ggplot and R - GitHub Pages
Aug 10, 2014 - Some terminology. ▷ data. ▷ aesthetics. ▷ geometry. ▷ The geometric objects in the plot. ▷ points, lines, polygons, etc. ▷ shortcut functions: geom point(), geom bar(), geom line(). Page 20. Basic structure ggplot(data = iris ...... Pa

Elastic computing with R and Redis - GitHub
May 16, 2016 - Listing 3 presents a parallel-capable variation of the boot function from the ... thisCall

CP2K with LIBXSMM - GitHub
make ARCH=Linux-x86-64-intel VERSION=psmp AVX=2. To target for instance “Knights ... //manual.cp2k.org/trunk/CP2K_INPUT/GLOBAL/DBCSR.html).

Java with Generators - GitHub
processes the control flow graph and transforms it into a state machine. This is required because we can then create states function SCOPEMANGLE(node).

OpenBMS connection with CAN - GitHub
Arduino with BMS- and CAN-bus shield as BMS a master. - LTC6802-2 or LTC6803-2 based boards as cell-level boards. - CAN controlled Eltek Valere as a ...

Better performance with WebWorkers - GitHub
Chrome52 on this Laptop. » ~14kbyte. String => 133ms ... 3-4 Seks processing time on samsung galaxy S5 with crosswalk to finish the transition with ... Page 17 ...

Getting Started with CodeXL - GitHub
10. Source Code View . ..... APU, a recent version of Radeon Software, and the OpenCL APP SDK. This document describes ...... lel_Processing_OpenCL_Programming_Guide-rev-2.7.pdf. For GPU ... trademarks of their respective companies.

Getting Started with Go - GitHub
Jul 23, 2015 - The majority of my experience is in PHP. I ventured into Ruby, ... Compiled, Statically Typed, Concurrent, Imperative language. Originally ...

Getting Acquainted with R - GitHub
In this case help.search(log) returns all the functions with the string 'log' in them. ... R environment your 'working directory' (i.e. the directory on your computer's file ... Later in the course we'll discuss some ways of implementing sanity check

Training ConvNets with Torch - GitHub
Jan 17, 2014 - ... features + SVM. – Neural Nets (and discuss discovering graph structure automařcally). – ConvNets. • Notebook Setup ... Page 9 ...

Examples with importance weights - GitHub
Page 3 ... Learning with importance weights y. wT t x wT t+1x s(h)||x||2 ... ∣p=(wt−s(h)x)Tx s (h) = η. ∂l(p,y). ∂p. ∣. ∣. ∣. ∣p=(wt−s(h)x)Tx. Finally s(0) = 0 ...

Digital Design with Chisel - GitHub
Dec 27, 2017 - This lecture notes (to become a book) are an introduction into hardware design with the focus on using the hardware construction language Chisel. The approach of this book is to present small to medium sized typical hardware components

Deep Learning with H2O.pdf - GitHub
best-in-class algorithms such as Random Forest, Gradient Boosting and Deep Learning at scale. .... elegant web interface or fully scriptable R API from H2O CRAN package. · grid search for .... takes to cut the learning rate in half (e.g., 10−6 mea

Financial Risk Modelling and Portfolio Optimization with R - GitHub
website of the R project is http://www.r-project.org. The source code of the software is published as free software under the terms of the GNU General Public ..... Eclipse Eclipse is a Java-based IDE and was first designed as an IDE for this ...... â

Puppet Enterprise on Azure with RHEL and Windows Nodes - GitHub
8. In the Deployment blade, scroll down to the Outputs section. You will see the Public IP address and DNS of Puppet Master VM and Public IP of Load Balancer ...

Workshop: Creating stencils with Inkscape and Potrace - GitHub
http://alternativeto.net/software/inkscape/ ... The “Fuzzy selector tool” helps a lot ... We are done with GIMP now we need to save the file in a format that potrace can ... the edges with a white brush. this way if I make a mistake i can recover

TransistorTester with AVR microcontroller and a little more ... - GitHub
but this forced analysing the whole program and change of all “if-statements” in the ...... 22Ω and 19Ω, the port resistor values are ignored for the computing of the ...... of three different ATmega168 processors are shown in figure 5.51a. 101

TransistorTester with AVR microcontroller and a little more ... - GitHub
If the serial output of text is not required, the Pin PC3 of the ATmega can be used as ... For that the external voltage (for example battery voltage) can be ...... The names for the connections of the display signals of the Arduino are ...... [19] h

On the Complexity and Performance of Parsing with ... - GitHub
seconds to parse only 31 lines of Python. ... Once these are fixed, PWD's performance improves to match that of other ...... usr/ftp/scan/CMU-CS-68-earley.pdf.

Architecting and Developing Modern Web Apps with ASP ... - GitHub
One Microsoft Way .... 3. Reference Application: eShopOnWeb . ...... Applying the dependency inversion principle allows A to call methods on an abstraction that B ...... Clients are not limited to browsers – mobile apps, console apps, and other ...