📜 ⬆️ ⬇️

Where do we form a model for UI with Domain Driven Design? Performance comparison of various architectural solutions


Consider from a performance point of view, options for locating logic to populate a model for three-tier and four-tier architectures when using different interaction technologies between levels on the .NET stack (Web API, Web API OData, WCF net.tcp, WCF Data Services).


Preamble



Often, when automating something, an approach to designing software based on a domain is used , the so-called Domain Driven Design . The essence of this approach is the creation of a program model of the domain containing the description of the objects of the domain, their relationships and possible operations with them. The presence of such a model makes it convenient to automate complex business processes.
')
As Fowler wrote in "Enterprise Software Architecture"

The value of the domain model is that, having mastered the approach, you have at your disposal a number of techniques that allow you to get along with the increasing complexity of business logic in a “civilized” way.


This design approach is object-oriented, so as a result we get a set of repository classes for performing CRUD and other operations on domain objects and DTO for transferring data from these objects.

Often these classes are implemented as a web service and allocated to a separate level (let's call it AppServer), with which different levels of display interact - web servers, mobile clients, etc. A complete implementation of this approach can be seen in the Pluralsight Building End-to-End Multi-Client Service Oriented Applications course.

Remark
Naming the course as it hints that they implement the SOA architecture. It seems to me that they are implementing it with great stretch, because, for example, in the book “Microsoft's Guide to Designing Application Architecture” , the main feature of the SOA architecture is clearly marked - service autonomy, the possibility of their distributed deployment, independent maintenance and development. IMHO service must have its own independent data storage to be part of SOA. The guys from Pluralsight just have the access facade to the domain model implemented as a set of WCF services, nothing more.


In other cases, these classes are simply a separate layer with which the presentation layer interacts.

However, almost always DTOs returned by such classes cannot serve as a model for the View UI, since contains either too much or too little data. This is partly due to the fact that the display levels are different (mobile clients, web servers) and they need different UI, although they work with the same domain model. Partly by the fact that just according to the requirements on a specific screen it is necessary to show the data of several objects of the domain.

Thus, it is obvious that the models for the UI are not displayed 1 in 1 on the DTO used to store the data of the domain objects, and therefore they need to be formed somewhere.

And where to form them and why is it important?



As it turns out, the place of their formation, as well as the inter-level interaction technologies used in this process, can easily make a good system a bad one in the eyes of users.

Remark
It is believed that a good automation system is different from a bad one in that it allows users to do their work faster than before .


Consider, from a performance point of view, the following options for forming a model for display in the UI:

Three-tier architecture (browser + webserver + database server)


Four-tier architecture (browser + webserver + appserver + database server)


So, we have 4 basic options.

To make it more interesting, we will use different technologies from the .NET stack for interaction between the levels.

And in order to make it very interesting - in the form of a model we will transfer the “real” amount of data (~ 150kb in uncompressed form), and the browser will be placed away from the main stand (good, the size of our Motherland allows it).

Used technologies and tools


The browser is Chrome 48, the model from the web server will be received in JSON using JQuery 2.

Web server - IIS 6.1, the model will render using Web API 2 (in one of the WebAPI OData v3 scenarios) using gzip compression. JSON serializer standard. The data will be received either from Appserver (in a four-tier architecture), or directly from the database using EF 6 (in a three-tier one).

Appserver - IIS 6.1, data will be given either using WCF Service (Net.TCP binding), or using WCF DataService v3 (without compression). C DB will communicate using EF 6.

Database server - MS SQL 2014 standart.

Level distribution by gland


Browser, web server, appserver, database server - all on different physical machines.

Iron configuration:


Web server, appserver, database server - in one network, geographically in Moscow. Connected by 10TB network. Webserver sticks to the Internet for Microsoft Forefront TMG.

The browser is outside, geographically in Moscow and Ufa (we will consider two options). Connected to the web server by the usual "good" Internet via WIFI.

Data


The database contains a simple table (Guid, Name) with 10 thousand data lines.

To form a model from this label using EF, three sets of data of 1000 rows are taken starting from an arbitrary value (Skip, Take). The first set is left unchanged, the second and third are filtered in the model's fill code. The total size of the rows of three sets is 2000 objects or ~ 150 KB.

Each data set is returned as a model. In this case, the first set is obtained synchronously, and the second and third - asynchronously.

Thus, both the database and the EF are present in us, but they contribute to the time of data acquisition in a minimum (since we test not the EF and not the database, but the architecture as a whole), but the size of the model is large.

What and how we measure


We measure the time in the browser between the beginning of the query model and the result in the form of javascript objects. Rendering of results in the browser is present, but not measured.

After rendering the model after waiting 1s, we repeat everything again.

Thus, we make ~ 500 measurements and build a graph of the distribution of results as a percentage, depending on time.

We will conduct two series of measurements simultaneously, placing the browsers geographically in different places (Moscow and Ufa)
Measurements will be carried out during working hours when channels and servers are regularly loaded.

Test benches


Thus we form the following test benches



Further description of each model and results, we go from the most brake to the fastest

Four-tier architecture, the model is formed on the web server, the appserver is accessible via WCF DataService, the result is transmitted to the browser via the Web API



Special features

Since the WCF DataService (which runs on top of the EF model) is used to access the application server, it is not necessary to write methods in this service to implement various data retrieval requests. Requests can be made via URL, or using LINQ

Model Data Acquisition Code
public class DocumentListController : ApiController { // GET: api/DocumentList/ public async Task<DocumentListViewModel> Get() { var result = new DocumentListViewModel(); var appServer = new DocumentsServiceHelper(); result.AllDocuments = await Task.Run(() => appServer.GetAllDocuments()); var data = await Task.WhenAll( Task.Run(() => appServer.GetEvenDocuments()), Task.Run(() => appServer.GetOddDocuments())); result.EvenDocuments = data[0]; result.OddDocuments = data[1]; return result; } } public class DocumentsServiceHelper { private const string DocumentsServiceUrl = @"http://xxx/appdataserver/documentsservice.svc/"; public DocumentItem[] GetAllDocuments() { var context = new Documents(new Uri(DocumentsServiceUrl)); var rnd = new Random(); return context.DocumentItems.OrderBy(x => x.Id).Skip(rnd.Next(0, 9000)).Take(1000).ToArray(); } public DocumentItem[] GetEvenDocuments() { var context = new Documents(new Uri(DocumentsServiceUrl)); var rnd = new Random(); return context.DocumentItems.OrderBy(x => x.Id).Skip(rnd.Next(0, 9000)).Take(1000).ToList().Where((x, i) => i % 2 != 0).ToArray(); } public DocumentItem[] GetOddDocuments() { var context = new Documents(new Uri(DocumentsServiceUrl)); var rnd = new Random(); return context.DocumentItems.OrderBy(x => x.Id).Skip(rnd.Next(0, 9000)).Take(1000).ToList().Where((x, i) => i % 2 == 0).ToArray(); } } } 


Service code
 [System.ServiceModel.ServiceBehavior(IncludeExceptionDetailInFaults = true)] public class DocumentsService : EntityFrameworkDataService<Documents> { public static void InitializeService(DataServiceConfiguration config) { config.SetEntitySetAccessRule("*", EntitySetRights.AllRead); config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V3; } } 



results


As you can see, almost all requests fall in the interval of 850-1100 ms, and about 5% are executed more than 1.1s. This is for Moscow. For Ufa, the picture is worse.

Conclusion

WCF Data Services is not our option

Three-tier architecture, the model is formed in the browser using a web server, accessible through the Odata Web API



Description

Since the model is formed using the OData Web API (which also works on top of the EF model), to implement various data retrieval requests, it is not necessary to write methods in the Web API controller. Requests can be made via URL.
Since the model is formed in the browser, three Web API requests go to the web server, unlike all other scenarios, where there are only one such requests.
Model formation code (on Type Script)
 class DocumentItem { Id: string; Name: string; constructor(_id: string, _name: string) { this.Id = _id; this.Name = _name; } } class DocumentsController { DocumentItems: DocumentItem[]; OddDocumentItems: DocumentItem[]; EvenDocumentItems: DocumentItem[]; constructor() { this.DocumentItems = []; this.EvenDocumentItems = []; this.OddDocumentItems = []; } public FillData(): Promise<any> { var p = new Promise<any>((resolve, reject) => { var queryOne = this.ExecuteQuery(this.GetQuery()) .then((data: DocumentItem[]) => { this.DocumentItems = data; var queryTwo = this.ExecuteQuery(this.GetQuery()); var queryThree = this.ExecuteQuery(this.GetQuery()); var result = Promise.all([queryTwo, queryThree]); result .then((data: DocumentItem[][]) => { this.EvenDocumentItems = data[0].filter((x, i, all) => i % 2 != 0); this.OddDocumentItems = data[1].filter((x, i, all) => i % 2 == 0); resolve(); }, reject); }, reject); }); return p; } private GetQuery(): string { var random = Math.floor(Math.random() * 9000); return "$orderby=Id desc&$skip=" + random + "&$top=1000"; } private ExecuteQuery(query: string): Promise<DocumentItem[]> { var uri = "odata/Documents?"; var p = new Promise((resolve, reject) => { $.getJSON(uri + query) .done((data: any) => { var realData = $.map<any, DocumentItem>(data.value, (x: any, i: number) => new DocumentItem(x.Id, x.Name)); resolve(realData); }) .fail((jqXHR: JQueryXHR, textStatus: string, err: string) => reject(err)); }); return p; } } 



On the server, the controller code is template (Web API 2 OData v3 Controller with actions using Entity Framework), I will not give it.

results


As you can see, almost all requests fall in the range of 200-350 ms for Moscow and 250-400 ms for Ufa, which is not so bad. BUT, there are also slow queries, more than 800 ms (for Ufa such about 5%). For the user, this will mean that the UI of the system works almost always quickly, but sometimes it will slow down. This is very annoying.

Conclusion

This architecture is now fashionable. And do not say that it is very slow. But IMHO it is suitable only for intranets, only for powerful office machines and only for desktop applications UI. Version, browser type - strongly influence the result. Under IE, all of this works significantly slower, I'm not talking about mobile browsers.

Four-tier architecture, the model is formed on the web server, the appserver is available via the WCF service using the net.tcp protocol, the result is transmitted to the browser via the Web API




Controller Web API Code
 // GET: api/DocumentList/ public async Task<DocumentListViewModel> Get() { var result = new DocumentListViewModel(); var appServer = new DocumentsService.DocumentsServiceClient(); appServer.Open(); result.AllDocuments = await appServer.GetAllDocumentsAsync(); var even = appServer.GetEvenDocumentsAsync(); var odd = appServer.GetOddDocumentsAsync(); var data = await Task.WhenAll(even, odd); result.EvenDocuments = data[0]; result.OddDocuments = data[1]; appServer.Close(); return result; } 



Service code
 public class DocumentsService : IDocumentsService { public DocumentItem[] GetAllDocuments() { var rnd = new Random(); using (var context = new Documents()) { return context.DocumentItems.OrderBy(x => x.Id).Skip(rnd.Next(0, 9000)).Take(1000).ToArray(); } } public DocumentItem[] GetEvenDocuments() { var rnd = new Random(); using (var context = new Documents()) { return context.DocumentItems.OrderBy(x => x.Id).Skip(rnd.Next(0, 9000)).Take(1000).ToList().Where((x, i) => i % 2 != 0).ToArray(); } } public DocumentItem[] GetOddDocuments() { var rnd = new Random(); using (var context = new Documents()) { return context.DocumentItems.OrderBy(x => x.Id).Skip(rnd.Next(0, 9000)).Take(1000).ToList().Where((x, i) => i % 2 == 0).ToArray(); } } } 



Result


As you can see, almost all requests fall in the range of 100-200 ms for Moscow and 150-250ms for Ufa. And, most importantly, there are no slow queries for Moscow, for Ufa - almost none.

Conclusion

With tz. performance - uniquely suitable architecture.

Four-tier architecture, the model is formed on the Appserver, which is accessible via the WCF service via the net.tcp protocol, the result is transmitted to the browser via the Web AP



Special features

Many will say that the model for UI to form on the appserver is moveton. So it, in fact, is.
To smooth the situation, we will not expand the facade of the DD model with methods for returning the model to the UI, but create a new service on the appserver that will use the facade of the DD model and form the model for the UI. The web server will use this particular service. If you need to implement a new UI, a new service must be added to the appserver.
In this scenario, the web server sends 1 WCF request to form a model, instead of three for all the other considered four-level scenarios.

Model formation code
 public class UIService : IUIService { public async Task<DocumentListViewModel> GetModel() { var result = new DocumentListViewModel(); var docService = new DocumentsService(); result.AllDocuments = await Task.Run(() => docService.GetAllDocuments()); var even = Task.Run(() => docService.GetEvenDocuments()); var odd = Task.Run(() => docService.GetOddDocuments()); var evenOdd = await Task.WhenAll(even, odd); result.EvenDocuments = evenOdd[0]; result.OddDocuments = evenOdd[1]; return result; } } } 



Controller Web API Code
 // GET: api/DocumentList/ public DocumentListViewModel Get() { var appServer = new UIServiceClient(); appServer.Open(); var data = appServer.GetModel(); appServer.Close(); return data; } 



results


As you can see, almost all requests fall in the range of 100-150 ms for Moscow and 150-250ms for Ufa.

Conclusion

The fastest four-tier architecture. And the most reliable, because the exchange between the web server and the appserver is minimized both by the amount of data and by the call frequency.

Three-tier architecture, the model is formed on the web server, the result is transmitted to the browser via the Web API



Controller code
 public class DocumentListController : ApiController { // GET: api/DocumentList/ public async Task<DocumentListViewModel> Get() { var result = new DocumentListViewModel(); var watch = new Stopwatch(); watch.Start(); var docService = new DocumentsService(); result.AllDocuments = await Task.Run(() => docService.GetAllDocuments()); var even = Task.Run(() => docService.GetEvenDocuments()); var odd = Task.Run(() => docService.GetOddDocuments()); var evenOdd = await Task.WhenAll(even, odd); result.EvenDocuments = evenOdd[0]; result.OddDocuments = evenOdd[1]; watch.Stop(); result.Log = watch.ElapsedMilliseconds.ToString(); return result; } } 



Local repository code (wrapper over EF)
 public class DocumentsService { public DocumentItem[] GetAllDocuments() { var rnd = new Random(); using (var context = new Documents()) { return context.DocumentItems.OrderBy(x => x.Id).Skip(rnd.Next(0, 9000)).Take(1000).ToArray(); } } public DocumentItem[] GetEvenDocuments() { var rnd = new Random(); using (var context = new Documents()) { return context.DocumentItems.OrderBy(x => x.Id).Skip(rnd.Next(0, 9000)).Take(1000).ToList().Where((x, i) => i % 2 != 0).ToArray(); } } public DocumentItem[] GetOddDocuments() { var rnd = new Random(); using (var context = new Documents()) { return context.DocumentItems.OrderBy(x => x.Id).Skip(rnd.Next(0, 9000)).Take(1000).ToList().Where((x, i) => i % 2 == 0).ToArray(); } } } 



Result


As you can see, almost all requests fall in the range of 100-150 ms for Moscow and 100-200 ms for Ufa.

Conclusion

If you don't need a four-tier architecture, then this is the fastest option.

So it goes.

Source: https://habr.com/ru/post/279331/


All Articles