forked from aspnetboilerplate/eventcloud
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy patharticle.html
972 lines (755 loc) · 41.8 KB
/
article.html
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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type" />
<link type="text/css" rel="stylesheet" href="bootstrap.min.css" />
</head>
<body>
<h1>A Multi-Tenant (SaaS) Application With ASP.NET MVC, Angularjs, EntityFramework and ASP.NET Boilerplate</h1>
<ul class="download">
<li><a href="EventCloudSource.zip">Download sample application</a> (or see latest on <a href="https://github.com/aspnetboilerplate/eventcloud" target="_blank">github)</a></li>
</ul>
<h2>Contents</h2>
<ul>
<li><a href="#ArticleIntroduction">Introduction</a></li>
<li><a href="#ArticleCreateTemplate">Creating Application From Template</a></li>
<li><a href="#ArticleEventCloudProject">Event Cloud Project</a>
<ul>
<li><a href="#ArticleEntities">Entities</a></li>
<li><a href="#ArticleEventRegistrationPolicy">Event Registration Policy</a></li>
<li><a href="#ArticleEventManager">Event Manager</a></li>
<li><a href="#ArticleDomainEvents">Domain Events</a></li>
<li><a href="#ArticleAppServices">Application Services</a></li>
<li><a href="#ArticlePresentation">Presentation Layer</a>
<ul>
<li><a href="#ArticleEventList">Event List</a></li>
<li><a href="#ArticleEventDetail">Event Detail</a></li>
<li><a href="#ArticleMainMenu">Main Menu</a></li>
<li><a href="#ArticleAngularRoute">Angular Route</a></li>
</ul>
</li>
<li><a href="#ArticleUnitTests">Unit and Integration Tests</a></li>
</ul>
</li>
<li><a href="#ArticleSocialLogins">Social Logins</a></li>
<li><a href="#ArticleTokenAuth">Token Based Authentication</a>
<ul>
<li><a href="#ArticleTokenAuthenticate">Authentication</a></li>
<li><a href="#ArticleTokenUseAPI">Use API</a></li>
</ul>
</li>
<li><a href="#ArticleSourceCode">Source Code</a></li>
<li><a href="#ArticleSummary">Summary</a></li>
<li><a href="#ArticleHistory">Article History</a></li>
</ul>
<p><img alt="Login page" src="login-page-v2.jpg" style="width:100%" /></p>
<p>See <a href="http://eventcloud.aspnetboilerplate.com/">LIVE DEMO</a>.</p>
<h2 id="ArticleIntroduction">Introduction</h2>
<p>In this article, we will see a SaaS (multi-tenant) application developed using the following frameworks:</p>
<ul>
<li><strong><a href="https://aspnetboilerplate.com/">ASP.NET Boilerplate</a></strong> as application framework.</li>
<li><strong>ASP.NET MVC</strong> and <strong>ASP.NET Web API</strong> as Web Frameworks.</li>
<li><strong>Entity Framework</strong> as ORM.</li>
<li><strong>Angularjs</strong> as SPA framework.</li>
<li><strong>Bootstrap</strong> as HTML/CSS framework.</li>
</ul>
<p>You can see <a href="http://eventcloud.aspnetboilerplate.com/">live demo</a> before reading the article. </p>
<h2 id="ArticleCreateTemplate">Creating Application From Template</h2>
<p>ASP.NET Boilerplate provides templates to make a project startup easier. We create the <strong>startup template</strong> from <a href="https://aspnetboilerplate.com/Templates"> https://aspnetboilerplate.com/Templates</a>:</p>
<p>
<img alt="Create template" height="781" src="create-template-3.png" width="955" /></p>
<p>I selected ASP.NET MVC 5.x, Angularjs and Entity Frameowork including "<strong>module zero</strong>". It creates a ready and working solution for us including a <strong>login page</strong>, <strong> navigation</strong> and a bootstrap based <strong>layout</strong>. After download and open the solution with <strong>Visual Studio 2015+</strong>, we see a <strong>layered</strong> solution structure including a unit test project:</p>
<p><img alt="Solution structure" height="167" src="solution-structure.png" width="215" /></p>
<p>First, we <strong>select EventCloud.Web as startup project</strong>. Solution comes with <strong>Entity Framework Code-First Migrations</strong>. So, (after restoring nuget packages) we open the <strong>Package Manager Console</strong> (PMC) and run <strong>Update-Database</strong> command to create the database:</p>
<p><img alt="Update database command" height="203" src="pmc-update-database.png" width="743" /></p>
<p>PMC <strong>Default project</strong> should be <strong> EventCloud.EntityFramework</strong> (since it contains the migrations). This command creates <strong>EventCloud </strong>database<strong> </strong>in local SQL Server (you can change <strong>connection string</strong> from <strong>web.config</strong> file).</p>
<p>Now, we can run the application. We see the pre-built <strong>login</strong> page. Can can enter <strong>default </strong>as tenancy name, <strong>admin </strong>as user and <strong>123qwe </strong>as password to login:</p>
<p><img alt="Initial Login PAge" height="394" src="initial-login-page.png" width="388" /></p>
<p>After login, we see the basic bootstrap based layout consists of two pages: <strong>Home</strong> and <strong>About</strong>:</p>
<p><img alt="Initial layout" height="449" src="initial-layout.png" width="956" /></p>
<p>This is a localized UI with a dynamic menu. Angular layout, routing and basic infrastructure are properly working. I got this project as a base for the event cloud project.</p>
<h2 id="ArticleEventCloudProject">Event Cloud Project</h2>
<p>In this article, I will show key parts of the project and explain it. So, please download the sample project, open in Visual Studio 2013+ and run migrations just like above before reading rest of the article (Be sure that there is no database named EventCloud before running the migrations). I will follow some <strong>DDD</strong> (Domain Driven Design) techniques to create <strong>domain (business) layer </strong>and <strong>application layer</strong>.</p>
<p>Event Cloud is a free SaaS (multi-tenant) application. We can create a tenant which has it's own events, users, roles... There are some simple business rules applied while creating, canceling and registering to an event.</p>
<p>So, let's start to investigate the source code.</p>
<h3 id="ArticleEntities">Entities</h3>
<p>Entities are parts of our domain layer and located under <strong> EventCloud.Core</strong> project. ASP.NET Boilerplate startup template comes with <strong>Tenant</strong>, <strong> User</strong>, <strong>Role</strong>... entities which are common for most applications. We can customize them based on our needs. Surely, we can add our application specific entities.</p>
<p>The fundamental entity of event cloud project is the <strong> Event </strong>entity:</p>
<pre lang="cs">
[Table("AppEvents")]
public class Event : FullAuditedEntity<Guid>, IMustHaveTenant
{
public const int MaxTitleLength = 128;
public const int MaxDescriptionLength = 2048;
public virtual int TenantId { get; set; }
[Required]
[StringLength(MaxTitleLength)]
public virtual string Title { get; protected set; }
[StringLength(MaxDescriptionLength)]
public virtual string Description { get; protected set; }
public virtual DateTime Date { get; protected set; }
public virtual bool IsCancelled { get; protected set; }
/// <summary>
/// Gets or sets the maximum registration count.
/// 0: Unlimited.
/// </summary>
[Range(0, int.MaxValue)]
public virtual int MaxRegistrationCount { get; protected set; }
[ForeignKey("EventId")]
public virtual ICollection<EventRegistration> Registrations { get; protected set; }
/// <summary>
/// We don't make constructor public and forcing to create events using <see cref="Create"/> method.
/// But constructor can not be private since it's used by EntityFramework.
/// Thats why we did it protected.
/// </summary>
protected Event()
{
}
public static Event Create(int tenantId, string title, DateTime date, string description = null, int maxRegistrationCount = 0)
{
var @event = new Event
{
Id = Guid.NewGuid(),
TenantId = tenantId,
Title = title,
Description = description,
MaxRegistrationCount = maxRegistrationCount
};
@event.SetDate(date);
@event.Registrations = new Collection<EventRegistration>();
return @event;
}
public bool IsInPast()
{
return Date < Clock.Now;
}
public bool IsAllowedCancellationTimeEnded()
{
return Date.Subtract(Clock.Now).TotalHours <= 2.0; //2 hours can be defined as Event property and determined per event
}
public void ChangeDate(DateTime date)
{
if (date == Date)
{
return;
}
SetDate(date);
DomainEvents.EventBus.Trigger(new EventDateChangedEvent(this));
}
internal void Cancel()
{
AssertNotInPast();
IsCancelled = true;
}
private void SetDate(DateTime date)
{
AssertNotCancelled();
if (date < Clock.Now)
{
throw new UserFriendlyException("Can not set an event's date in the past!");
}
if (date <= Clock.Now.AddHours(3)) //3 can be configurable per tenant
{
throw new UserFriendlyException("Should set an event's date 3 hours before at least!");
}
Date = date;
DomainEvents.EventBus.Trigger(new EventDateChangedEvent(this));
}
private void AssertNotInPast()
{
if (IsInPast())
{
throw new UserFriendlyException("This event was in the past");
}
}
private void AssertNotCancelled()
{
if (IsCancelled)
{
throw new UserFriendlyException("This event is canceled!");
}
}
}</pre>
<p>Event entity has not just get/set properties. Actually, it has not <strong>public setters</strong>, setters are protected. It has some <strong>domain logic</strong>. All properties must be changed by the Event entity itself to ensure domain logic is executed.</p>
<p>Event entity's <strong>constructor</strong> is also <strong>protected</strong>. So, the only way to create an Event is the <strong>Event.Create</strong> method (They can be private normally, but private setters don't work well with Entity Framework since Entity Framework can not set privates when retrieving an entity from database).</p>
<p>Event implements <strong>IMustHaveTenant</strong> interface. This is an interface of ASP.NET Boilerplate (ABP) framework and ensures that this entity is per tenant. This is needed for <strong>multi-tenancy</strong>. Thus, different tenants will have different events and can not see each other's events. ABP automatically filters entities of current tenant.</p>
<p>Event class inherits from <strong>FullAuditedEntity</strong> which contains creation, modification and deletion audit columns. FullAuditedEntity also implements <strong>ISoftDelete</strong>, so events can not be deleted from database. They are marked as deleted when you delete it. ABP automatically filters (hides) deleted entities when you query database.</p>
<p>In DDD, entities have domain (business) logic. We have some simple business rules those can be understood easily when you check the entity.</p>
<p>Second entity of our application is <strong>EventRegistration</strong>:</p>
<pre lang="cs">
[Table("AppEventRegistrations")]
public class EventRegistration : CreationAuditedEntity, IMustHaveTenant
{
public int TenantId { get; set; }
[ForeignKey("EventId")]
public virtual Event Event { get; protected set; }
public virtual Guid EventId { get; protected set; }
[ForeignKey("UserId")]
public virtual User User { get; protected set; }
public virtual long UserId { get; protected set; }
/// <summary>
/// We don't make constructor public and forcing to create registrations using <see cref="CreateAsync"/> method.
/// But constructor can not be private since it's used by EntityFramework.
/// Thats why we did it protected.
/// </summary>
protected EventRegistration()
{
}
public async static Task<EventRegistration> CreateAsync(Event @event, User user, IEventRegistrationPolicy registrationPolicy)
{
await registrationPolicy.CheckRegistrationAttemptAsync(@event, user);
return new EventRegistration
{
TenantId = @event.TenantId,
EventId = @event.Id,
Event = @event,
UserId = @user.Id,
User = user
};
}
public async Task CancelAsync(IRepository<EventRegistration> repository)
{
if (repository == null) { throw new ArgumentNullException("repository"); }
if (Event.IsInPast())
{
throw new UserFriendlyException("Can not cancel event which is in the past!");
}
if (Event.IsAllowedCancellationTimeEnded())
{
throw new UserFriendlyException("It's too late to cancel your registration!");
}
await repository.DeleteAsync(this);
}
}</pre>
<p>As similar to Event, we have a static create method. The only way of creating a new EventRegistration is this <strong>CreateAsync</strong> method. It gets an <strong>event</strong>, <strong>user</strong> and a <strong>registration policy</strong>. It checks if given user can register to the event using registrationPolicy.<strong>CheckRegistrationAttemptAsync</strong> method. This method throws exception if given user can not register to given event. With such a design, we ensure that all business rules are applied while creating a registration. There is no way of creating a registration without using registration policy.</p>
<p>See <a href="http://aspnetboilerplate.com/Pages/Documents/Entities">Entity documentation</a> for more information on entities.</p>
<h3 id="ArticleEventRegistrationPolicy">Event Registration Policy</h3>
<p><strong>EventRegistrationPolicy</strong> class is defined as shown below:</p>
<pre lang="cs">
public class EventRegistrationPolicy : EventCloudServiceBase, IEventRegistrationPolicy
{
private readonly IRepository<EventRegistration> _eventRegistrationRepository;
public EventRegistrationPolicy(IRepository<EventRegistration> eventRegistrationRepository)
{
_eventRegistrationRepository = eventRegistrationRepository;
}
public async Task CheckRegistrationAttemptAsync(Event @event, User user)
{
if (@event == null) { throw new ArgumentNullException("event"); }
if (user == null) { throw new ArgumentNullException("user"); }
CheckEventDate(@event);
await CheckEventRegistrationFrequencyAsync(user);
}
private static void CheckEventDate(Event @event)
{
if (@event.IsInPast())
{
throw new UserFriendlyException("Can not register event in the past!");
}
}
private async Task CheckEventRegistrationFrequencyAsync(User user)
{
var oneMonthAgo = Clock.Now.AddDays(-30);
var maxAllowedEventRegistrationCountInLast30DaysPerUser = await SettingManager.GetSettingValueAsync<int>(EventCloudSettingNames.MaxAllowedEventRegistrationCountInLast30DaysPerUser);
if (maxAllowedEventRegistrationCountInLast30DaysPerUser > 0)
{
var registrationCountInLast30Days = await _eventRegistrationRepository.CountAsync(r => r.UserId == user.Id && r.CreationTime >= oneMonthAgo);
if (registrationCountInLast30Days > maxAllowedEventRegistrationCountInLast30DaysPerUser)
{
throw new UserFriendlyException(string.Format("Can not register to more than {0}", maxAllowedEventRegistrationCountInLast30DaysPerUser));
}
}
}
}</pre>
<p>This is an important part of our domain. We have two rules while creating an event registration:</p>
<ol>
<li>A used can not register to an event <strong>in the past</strong>.</li>
<li>A user can register to a <strong>maximum count </strong>of events in 30 days. So, we have registration frequency limit.</li>
</ol>
<h3 id="ArticleEventManager">Event Manager</h3>
<p><strong>EventManager</strong> implements business (domain) logic for events. All Event operations should be executed using this class. It's defined as shown below:</p>
<pre lang="cs">
public class EventManager : IEventManager
{
public IEventBus EventBus { get; set; }
private readonly IEventRegistrationPolicy _registrationPolicy;
private readonly IRepository<EventRegistration> _eventRegistrationRepository;
private readonly IRepository<Event, Guid> _eventRepository;
public EventManager(
IEventRegistrationPolicy registrationPolicy,
IRepository<EventRegistration> eventRegistrationRepository,
IRepository<Event, Guid> eventRepository)
{
_registrationPolicy = registrationPolicy;
_eventRegistrationRepository = eventRegistrationRepository;
_eventRepository = eventRepository;
EventBus = NullEventBus.Instance;
}
public async Task<Event> GetAsync(Guid id)
{
var @event = await _eventRepository.FirstOrDefaultAsync(id);
if (@event == null)
{
throw new UserFriendlyException("Could not found the event, maybe it's deleted!");
}
return @event;
}
public async Task CreateAsync(Event @event)
{
await _eventRepository.InsertAsync(@event);
}
public void Cancel(Event @event)
{
@event.Cancel();
EventBus.Trigger(new EventCancelledEvent(@event));
}
public async Task<EventRegistration> RegisterAsync(Event @event, User user)
{
return await _eventRegistrationRepository.InsertAsync(
await EventRegistration.CreateAsync(@event, user, _registrationPolicy)
);
}
public async Task CancelRegistrationAsync(Event @event, User user)
{
var registration = await _eventRegistrationRepository.FirstOrDefaultAsync(r => r.EventId == @event.Id && r.UserId == user.Id);
if (registration == null)
{
//No need to cancel since there is no such a registration
return;
}
await registration.CancelAsync(_eventRegistrationRepository);
}
public async Task<IReadOnlyList<User>> GetRegisteredUsersAsync(Event @event)
{
return await _eventRegistrationRepository
.GetAll()
.Include(registration => registration.User)
.Where(registration => registration.EventId == @event.Id)
.Select(registration => registration.User)
.ToListAsync();
}
}</pre>
<p>It performs domain logic and triggers needed events.</p>
<p>See <a href="http://aspnetboilerplate.com/Pages/Documents/Domain-Services"> domain services documentation</a> for more information on domain services.</p>
<h3 id="ArticleDomainEvents">Domain Events</h3>
<p>We may want to define and trigger some domain specific events on some state changes in our application. I defined 2 domain specific events:</p>
<ul>
<li><strong>EventCancelledEvent</strong>: Triggered when an event is canceled. It's triggered in <strong>EventManager.Cancel</strong> method.</li>
<li><strong>EventDateChangedEvent</strong>: Triggered when date of an event changed. It's triggered in <strong>Event.ChangeDate</strong> method.</li>
</ul>
<p>We handle these events and notify related users about these changes. Also, I handle <strong>EntityCreatedEventDate<Event></strong> (which is a pre-defined ABP event and triggered automatically).</p>
<p>To handle an event, we should define an event handler class. I defined EventUserEmailer to send emails to users when needed:</p>
<pre lang="cs">
public class EventUserEmailer :
IEventHandler<EntityCreatedEventData<Event>>,
IEventHandler<EventDateChangedEvent>,
IEventHandler<EventCancelledEvent>,
ITransientDependency
{
public ILogger Logger { get; set; }
private readonly IEventManager _eventManager;
private readonly UserManager _userManager;
public EventUserEmailer(
UserManager userManager,
IEventManager eventManager)
{
_userManager = userManager;
_eventManager = eventManager;
Logger = NullLogger.Instance;
}
[UnitOfWork]
public virtual void HandleEvent(EntityCreatedEventData<Event> eventData)
{
//TODO: Send email to all tenant users as a notification
var users = _userManager
.Users
.Where(u => u.TenantId == eventData.Entity.TenantId)
.ToList();
foreach (var user in users)
{
var message = string.Format("Hey! There is a new event '{0}' on {1}! Want to register?",eventData.Entity.Title, eventData.Entity.Date);
Logger.Debug(string.Format("TODO: Send email to {0} -> {1}", user.EmailAddress, message));
}
}
public void HandleEvent(EventDateChangedEvent eventData)
{
//TODO: Send email to all registered users!
var registeredUsers = AsyncHelper.RunSync(() => _eventManager.GetRegisteredUsersAsync(eventData.Entity));
foreach (var user in registeredUsers)
{
var message = eventData.Entity.Title + " event's date is changed! New date is: " + eventData.Entity.Date;
Logger.Debug(string.Format("TODO: Send email to {0} -> {1}",user.EmailAddress, message));
}
}
public void HandleEvent(EventCancelledEvent eventData)
{
//TODO: Send email to all registered users!
var registeredUsers = AsyncHelper.RunSync(() => _eventManager.GetRegisteredUsersAsync(eventData.Entity));
foreach (var user in registeredUsers)
{
var message = eventData.Entity.Title + " event is canceled!";
Logger.Debug(string.Format("TODO: Send email to {0} -> {1}", user.EmailAddress, message));
}
}
}</pre>
<p>We can handle same events in different classes or different events in same class (as in this sample). Here, we handle these events and send email to related users as a notification (not implemented emailing actually to make the sample application simpler). An event handler should implement IEventHandler<<em>event-type</em>> interface. ABP automatically calls the handler when related events occur.</p>
<p>See <a href="http://aspnetboilerplate.com/Pages/Documents/EventBus-Domain-Events"> EventBus documentation</a> for more information on domain events.</p>
<h3 id="ArticleAppServices">Application Services</h3>
<p>Application services use domain layer to implement use cases of the application (generally used by presentation layer). <strong> EventAppService</strong> performs application logic for events.</p>
<pre lang="cs">
[AbpAuthorize]
public class EventAppService : EventCloudAppServiceBase, IEventAppService
{
private readonly IEventManager _eventManager;
private readonly IRepository<Event, Guid> _eventRepository;
public EventAppService(
IEventManager eventManager,
IRepository<Event, Guid> eventRepository)
{
_eventManager = eventManager;
_eventRepository = eventRepository;
}
public async Task<ListResultOutput<EventListDto>> GetList(GetEventListInput input)
{
var events = await _eventRepository
.GetAll()
.Include(e => e.Registrations)
.WhereIf(!input.IncludeCanceledEvents, e => !e.IsCancelled)
.OrderByDescending(e => e.CreationTime)
.ToListAsync();
return new ListResultOutput<EventListDto>(events.MapTo<List<EventListDto>>());
}
public async Task<EventDetailOutput> GetDetail(EntityRequestInput<Guid> input)
{
var @event = await _eventRepository
.GetAll()
.Include(e => e.Registrations)
.Where(e => e.Id == input.Id)
.FirstOrDefaultAsync();
if (@event == null)
{
throw new UserFriendlyException("Could not found the event, maybe it's deleted.");
}
return @event.MapTo<EventDetailOutput>();
}
public async Task Create(CreateEventInput input)
{
var @event = Event.Create(AbpSession.GetTenantId(), input.Title, input.Date, input.Description, input.MaxRegistrationCount);
await _eventManager.CreateAsync(@event);
}
public async Task Cancel(EntityRequestInput<Guid> input)
{
var @event = await _eventManager.GetAsync(input.Id);
_eventManager.Cancel(@event);
}
public async Task<EventRegisterOutput> Register(EntityRequestInput<Guid> input)
{
var registration = await RegisterAndSaveAsync(
await _eventManager.GetAsync(input.Id),
await GetCurrentUserAsync()
);
return new EventRegisterOutput
{
RegistrationId = registration.Id
};
}
public async Task CancelRegistration(EntityRequestInput<Guid> input)
{
await _eventManager.CancelRegistrationAsync(
await _eventManager.GetAsync(input.Id),
await GetCurrentUserAsync()
);
}
private async Task<EventRegistration> RegisterAndSaveAsync(Event @event, User user)
{
var registration = await _eventManager.RegisterAsync(@event, user);
await CurrentUnitOfWork.SaveChangesAsync();
return registration;
}
}</pre>
<p>As you see, application service does not implement domain logic itself. It just uses entities and domain services (EventManager) to perform the use cases.</p>
<p>See <a href="http://aspnetboilerplate.com/Pages/Documents/Application-Services"> application service documentation</a> for more information on application services.</p>
<h3 id="ArticlePresentation">Presentation Layer</h3>
<p>Presentation layer for this application is built using Angularjs as a SPA.</p>
<h4 id="ArticleEventList">Event List</h4>
<p>When we login to the application, we first see a list of events:</p>
<p><img alt="Event list page" height="396" src="event-list-page.png" width="959" /></p>
<p>We directly use <strong>EventAppService</strong> to get list of events. Here, the Angular controller to create this page:</p>
<pre lang="js">
(function() {
var controllerId = 'app.views.events.index';
angular.module('app').controller(controllerId, [
'$scope', '$modal', 'abp.services.app.event',
function ($scope, $modal, eventService) {
var vm = this;
vm.events = [];
vm.filters = {
includeCanceledEvents: false
};
function loadEvents() {
<strong> eventService.getList(vm.filters).success(function (result) {
vm.events = result.items;
});
</strong> };
vm.openNewEventDialog = function() {
var modalInstance = $modal.open({
templateUrl: abp.appPath + 'App/Main/views/events/createDialog.cshtml',
controller: 'app.views.events.createDialog as vm',
size: 'md'
});
modalInstance.result.then(function () {
loadEvents();
});
};
$scope.$watch('vm.filters.includeCanceledEvents', function (newValue, oldValue) {
if (newValue != oldValue) {
loadEvents();
}
});
loadEvents();
}
]);
})();</pre>
<p>We inject <strong>EventAppService</strong> as '<strong>abp.services.app.event</strong>' into Angular controller. We used <a href="http://www.aspnetboilerplate.com/Pages/Documents/Dynamic-Web-API"> dynamic web api layer</a> feature of ABP. It creates needed Web API controller and Angularjs service <strong>automatically</strong> and <strong>dynamically</strong>. So, we can use application service methods like calling regular javascript functions. So, to call <strong>EventAppService.GetList</strong> C# method, we simple call <strong>eventService.getList</strong> javascript function which returns a <strong>promise</strong> ($q for angular).</p>
<p>We also open a "new event" modal (dialog) when user clicks to "+ New event" button (which triggers vm.openNewEventDialog function). I will not go details of Angular views, since they are simpler and you can check it in source code.</p>
<h4 id="ArticleEventDetail">Event Detail</h4>
<p>When we click "Details" button for an event, we go to event details with a URL like "http://eventcloud.aspnetboilerplate.com/<strong>#/events/e9499e3e-35c0-492c-98ce-7e410461103f</strong>". GUID is id of the event.</p>
<p><img alt="Event details" height="473" src="event-detail-page.png" width="742" /></p>
<p>Here, we see event details with registered users. We can register to the event or cancel registration. This view's controller is defined in detail.js as shown below:</p>
<pre lang="js">
(function () {
var controllerId = 'app.views.events.detail';
angular.module('app').controller(controllerId, [
'$scope', '$state','$stateParams', 'abp.services.app.event',
function ($scope, $state, $stateParams, eventService) {
var vm = this;
function loadEvent() {
<strong> eventService.getDetail({
id: $stateParams.id
}).success(function (result) {
vm.event = result;
});
</strong> }
vm.isRegistered = function () {
if (!vm.event) {
return false;
}
return _.find(vm.event.registrations, function(registration) {
return registration.userId == abp.session.userId;
});
};
vm.isEventCreator = function() {
return vm.event && vm.event.creatorUserId == abp.session.userId;
};
vm.getUserThumbnail = function(registration) {
return registration.userName.substr(0, 1).toLocaleUpperCase();
};
vm.register = function() {
<strong> eventService.register({
id: vm.event.id
}).success(function (result) {
abp.notify.success('Successfully registered to event. Your registration id: ' + result.registrationId + ".");
loadEvent();
});
</strong> };
vm.cancelRegistertration = function() {
<strong> eventService.cancelRegistration({
id: vm.event.id
}).success(function () {
abp.notify.info('Canceled your registration.');
loadEvent();
});
</strong> };
vm.cancelEvent = function() {
<strong> eventService.cancel({
id: vm.event.id
}).success(function () {
abp.notify.info('Canceled the event.');
vm.backToEventsPage();
});
</strong> };
vm.backToEventsPage = function() {
$state.go('events');
};
loadEvent();
}
]);
})();</pre>
<p>We simply use event application service to perform actions.</p>
<h4 id="ArticleMainMenu">Main Menu</h4>
<p>Top menu is automatically created by ABP template. We define menu items in EventCloudNavigationProvider class:</p>
<pre lang="cs">
public class EventCloudNavigationProvider : NavigationProvider
{
public override void SetNavigation(INavigationProviderContext context)
{
context.Manager.MainMenu
.AddItem(
new MenuItemDefinition(
AppPageNames.Events,
new LocalizableString("Events", EventCloudConsts.LocalizationSourceName),
url: "#/",
icon: "fa fa-calendar-check-o"
)
).AddItem(
new MenuItemDefinition(
AppPageNames.About,
new LocalizableString("About", EventCloudConsts.LocalizationSourceName),
url: "#/about",
icon: "fa fa-info"
)
);
}
}</pre>
<p>We can add new menu items here. See <a href="http://www.aspnetboilerplate.com/Pages/Documents/Navigation">navigation documentation</a> for more information.</p>
<h4 id="ArticleAngularRoute">Angular Route</h4>
<p>Defining the menu only shows it on the page. Angular has it's own route system. This application uses Angular ui-router. Routes are defined in app.js as shown below:</p>
<pre lang="js">
//Configuration for Angular UI routing.
app.config([
'$stateProvider', '$urlRouterProvider',
function($stateProvider, $urlRouterProvider) {
$urlRouterProvider.otherwise('/events');
$stateProvider
.state('events', {
url: '/events',
templateUrl: '/App/Main/views/events/index.cshtml',
menu: 'Events' //Matches to name of 'Events' menu in EventCloudNavigationProvider
})
.state('eventDetail', {
url: '/events/:id',
templateUrl: '/App/Main/views/events/detail.cshtml',
menu: 'Events' //Matches to name of 'Events' menu in EventCloudNavigationProvider
})
.state('about', {
url: '/about',
templateUrl: '/App/Main/views/about/about.cshtml',
menu: 'About' //Matches to name of 'About' menu in EventCloudNavigationProvider
});
}
]);</pre>
<h2 id="ArticleUnitTests">Unit and Integration Tests</h2>
<p>ASP.NET Boilerplate provides tools to make unit and integration tests easier. You can find all test code from <a href="https://github.com/aspnetboilerplate/eventcloud">source code</a> of the project. Here, I will briefly explain basic tests. Solution includes <strong>EventAppService_Tests</strong> class which tests the EventAppService. See 2 tests from this class:</p>
<pre lang="js">
public class EventAppService_Tests : EventCloudTestBase
{
private readonly IEventAppService _eventAppService;
public EventAppService_Tests()
{
_eventAppService = Resolve<IEventAppService>();
}
[Fact]
<strong>public async Task Should_Create_Event()</strong>
{
//Arrange
var eventTitle = Guid.NewGuid().ToString();
//Act
await _eventAppService.Create(new CreateEventInput
{
Title = eventTitle,
Description = "A description",
Date = Clock.Now.AddDays(2)
});
//Assert
UsingDbContext(context =>
{
context.Events.FirstOrDefault(e => e.Title == eventTitle).ShouldNotBe(null);
});
}
[Fact]
<strong>public async Task Should_Not_Create_Events_In_The_Past()</strong>
{
//Arrange
var eventTitle = Guid.NewGuid().ToString();
//Act
await Assert.ThrowsAsync<UserFriendlyException>(async () =>
{
await _eventAppService.Create(new CreateEventInput
{
Title = eventTitle,
Description = "A description",
Date = Clock.Now.AddDays(-1)
});
});
}
private Event GetTestEvent()
{
return UsingDbContext(context => GetTestEvent(context));
}
private static Event GetTestEvent(EventCloudDbContext context)
{
return context.Events.Single(e => e.Title == TestDataBuilder.TestEventTitle);
}
}</pre>
<p>We use <strong>xUnit</strong> as test framework. In the first test, we simply create an event and check database if it's in there. In the second test, we intentionally trying to create an event in the past. Since our business rule don't allow it, we should get an exception here.</p>
<p>With such tests, we tested everyting starting from application service including all aspects of ASP.NET Boilerplate (like validation, unit of work and so on). See my <em> <a href="http://www.codeproject.com/Articles/871786/Unit-testing-in-Csharp-using-xUnit-Entity-Framewor">Unit testing in C# using xUnit, Entity Framework, Effort and ASP.NET Boilerplate</a></em> article for details on unit testing.</p>
<h2 id="ArticleSocialLogins">Social Logins</h2>
<p>Startup template is configured to work with social login providers: Facebook, Twitter and Google+. All we need to enable it in web.config and enter API credentials:</p>
<pre lang="xml">
<add key="ExternalAuth.Facebook.IsEnabled" value="false" />
<add key="ExternalAuth.Facebook.AppId" value="" />
<add key="ExternalAuth.Facebook.AppSecret" value="" />
<add key="ExternalAuth.Twitter.IsEnabled" value="false" />
<add key="ExternalAuth.Twitter.ConsumerKey" value="" />
<add key="ExternalAuth.Twitter.ConsumerSecret" value="" />
<add key="ExternalAuth.Google.IsEnabled" value="false" />
<add key="ExternalAuth.Google.ClientId" value="" />
<add key="ExternalAuth.Google.ClientSecret" value="" /></pre>
<p>You can easily find information on the web to learn how to get these credentials from vendors.</p>
<h2 id="ArticleTokenAuth">Token Based Authentication</h2>
<p>Startup template uses cookie based authentication for browsers. However, if you want to consume Web APIs or application services (those are exposed via <a href="http://www.aspnetboilerplate.com/Pages/Documents/Dynamic-Web-API">dynamic web api</a>) from a mobile application, you probably want a token based authentication mechanism. Startup template includes bearer token authentication infrastructure. <strong>AccountController</strong> in <strong>.WebApi</strong> project contains <strong>Authenticate</strong> action to get the token. Then we can use the token for next requests.</p>
<p>Here, <strong>Postman</strong> (chrome extension) will be used to demonstrate requests and responses.</p>
<h3 id="ArticleTokenAuthenticate">Authentication</h3>
<p>Just send a <strong>POST</strong> request to <strong> http://localhost:6334/api/Account/Authenticate</strong> with <strong> Context-Type="application/json"</strong> header as shown below:</p>
<p><img alt="Token based auth" height="718" src="token-auth.png" width="713" /></p>
<p>We sent a <strong>JSON request body</strong> includes <strong>tenancyName</strong>, <strong>userNameOrEmailAddress </strong>and <strong>password</strong>. <strong>tenancyName </strong>is not required for <strong>host</strong> users. As seen above, <strong>result</strong> property of returning JSON contains the token. We can save it and use for next requests.</p>
<h3 id="ArticleTokenUseAPI">Use API</h3>
<p>After authenticate and get the <strong>token</strong>, we can use it to call any <strong>authorized</strong> action. All <strong>application services </strong>are available to be used remotely. For example, we can use the <strong>EventAppService</strong> to get a <strong> list of events</strong>:</p>
<p><img alt="Use application service via token" height="914" src="token-api-call.png" width="700" /></p>
<p>Just made a <strong>POST</strong> request to <strong> http://localhost:6334/api/services/app/event/GetList</strong> with <strong> Content-Type="application/json"</strong> and <strong>Authorization="Bearer <em> your-</em></strong><em><strong>auth-token</strong></em><strong>"</strong>. Request body was just empty <strong>{}</strong>. Surely, request and response body will be different for different APIs.</p>
<p>Almost all operations available on UI also available as Web API (since UI uses the same Web API) and can be consumed easily.</p>
<h2 id="ArticleSourceCode">Source Code</h2>
<p>You can get the latest source code here: <a href="https://github.com/aspnetboilerplate/eventcloud"> https://github.com/aspnetboilerplate/eventcloud</a></p>
<h2 id="ArticleSummary">Summary</h2>
<p>In this article, I introduced a Multi Tenant (SaaS) application built on <a href="https://aspnetboilerplate.com">ASP.NET Boilerplate</a> (ABP) framework. Use the following links for more information on ASP.NET Boilerplate:</p>
<ul>
<li>Official site and documentation: <a href="https://aspnetboilerplate.com">aspnetboilerplate.com</a></li>
<li>Github repositories: <a href="https://github.com/aspnetboilerplate">github.com/aspnetboilerplate</a></li>
<li>Follow on twitter: <a href="https://twitter.com/aspboilerplate"> @aspboilerplate</a></li>
</ul>
<h2 id="ArticleHistory">Article History</h2>
<ul>
<li>2017-06-28<ul>
<li>Upgraded to ABP v2.1.3.</li>
<li>Updated project creation section.</li>
</ul>
</li>
<li>2016-07-19
<ul>
<li>Renewed images and revised content.</li>
<li>Added statistics to about page.</li>
<li>Upgraded Abp.* nuget packages to v0.10.</li>
</ul>
</li>
<li>2016-01-08
<ul>
<li>Added 'unit and integration tests' section.</li>
<li>Upgraded Abp.* nuget packages to v0.7.7.1.</li>
</ul>
</li>
<li>2015-12-04
<ul>
<li>Added 'social media login' and 'token based authentication' sections.</li>
<li>Localized UI.</li>
<li>Upgraded to .NET framework 4.5.2.</li>
<li>Updated Abp.* nuget packages to v0.7.5.</li>
</ul>
</li>
<li>2015-10-26
<ul>
<li>First publish of the article.</li>
</ul>
</li>
</ul>
</body>
</html>