基于DDD的.NET开发框架ABP实例,多租户 (Saas)应用程序,采用.NET MVC, Angularjs, EntityFrame-EventCloud
2017-02-20 13:40
831 查看
活动云项目
在本文中,我们将展示本项目的关键部分并且给予注释信息和说明。建议从网站模板中输入“EventCloud”,下载并且使用Vistual Studio 2013+的版本打开。我将遵循一些DDD(领域驱动设计)的技术来进行创建领域层和应用层。
Event Cloud是一个免费的SaaS(多租户)应用程序。我们可以创建一个拥有自己的活动,用户,角色,租户,版本,创建、取消和参与活动的一些简单的业务规则。
现在我们开始写代码吧。
# 实体[Entities]
实体文件信息包含在领域层,位于EventCloud.Core项目中。ASP.NET Boilerplate启动模板自带的Tenant,User,Role ...实体是zero模块中封装好了的常用实体。我们可以根据我们的需要定制它们。当然,我们可以给自己的程序添加特定的实体信息。
## 第一个实体:Event
[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!"); } } }
Event实体具有set/get属性,它没有public(公共set属性) ,他的set属性是被保护起来了(protected)。它还有一些领域逻辑。所有属性都必须满足它自身的领域逻辑之后才能正常的执行。
Event实体的构造函数也是Protected。所以创建活动的唯一方法就是Event.Create方法(我们这里不把他设置为private 私有方法。因为私有方法不能很好地与EF框架一起使用,因为从数据库查询实体时,Entity Framework不能设置私有)。
Event 需要实现 IMustHaveTenant接口。这个是ABP框架的接口,它可以确保这个实体是每个租户都可以使用。这个是多租户需要的。因此,不同的租户将具有不同 的事件,并且不会看到彼此的活动信息。ABP自动过滤当前租户的实体信息。
Event实体继承FullAuditedEntity,它包含创建,修改,删除审计字段。FullAuditedEntity也实现了ISoftDelete,所以事件不能从数据库中删除。当您删除它们的时候,它们会被标记为已删除。当您查询数据库的时候,ABP会自动过滤(隐藏)已删除的实体信息。
在DDD中,实体拥有领域(业务)逻辑。我们有一些简单的业务规则,当你检查实体时,可以很容易地理解。
第二个实体:EventRegistration
[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); } }
与Event类似,我们有一个静态create方法。创建新的EventRegistration的唯一方法是CreateAsync方法。它获得一个event,user和参加的逻辑处理。它检查该用户是否可以使用registrationPolicy参与到活动中。CheckRegistrationAttemptAsync方法,我为了保证如果该用户不能参与到该活动中,该方法就会弹出异常。通过这样的业务设计,我们可以确保只有该方法可以来创建
如果给定用户无法注册到给定事件,此方法将抛出异常。通过这样的设计,我们确保在创建注册时应用所有业务规则。没有使用注册政策,没有办法创建注册。
有关实体的更多信息,请参阅实体文档。
业务逻辑:EventRegistrationPolicy
EventRegistrationPolicy 代码: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)); } } } }
用户无法参与过期(结束)的活动
用户30天内,参与活动有最大参与活动数量的限制。
领域服务:EventManager
EventManager 作为Event的业务领域逻辑。所有活动的(数据库)操作都应该使用这个类来执行。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(); } }
领域服务用于执行业务逻辑处理完毕之后的方法。
有关ABP的领域服务的详细信息,可以参阅领域服务
领域活动(Domain Event)
我们可能需要一些特殊的业务处理情景来满足我们的系统,这个时候就需要我们来定义一些特殊的事件。EventCancelledEvent:当活动被取消时触发。它在EventManager.Cancel方法中触发。
EventDateChangedEvent:当活动的日期更改时触发。它在Event.ChangeDate方法中触发。
我们处理这些活动并会通知相关用户(已经参与该活动的用户)发生的变化。会通过ABP框架定义好的事件:EntityCreatedEventDate 来进行处理
要处理一个事件,我们定义一个事件处理类,我们定义一个EventUserEmailer,用来处理需要给用户发送电子邮件:
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)); } } }
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 interface. ABP automatically calls the handler when related events occur.
处理同一个类中的不同事件,或者不同事件中的相同类(在本例中)。在这里我们可以给这些活动有关的所有都发送邮件通知信息过去(不实现电子邮件功能,我们的这个例子会更加的简单)。
我们可以处理不同的类中的同一事件或同一类 (如本示例) 中的不同事件。在这里,我们处理这些事件并发送电子邮件给相关用户作为通知 (不实现电子邮件实际上为了使示例应用程序更简单)。事件处理程序应实现 IEventHandler接口。ABP框架会自动处理调用实现了这些接口的方法。
有关领域事件的具体更多信息,请参考文档:领域事件。
应用层服务
应用层服务通过是调用领域层的方法,来实现服务(通常是通过展现层表示出来)。EventAppService 是执行活动逻辑业务的方法。[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; } }
应用层服务未实现领域业务逻辑本身,他只是调用实体和领域服务(EventManager)来执行,实现功能需求。
展现层
使用angular js与bootstrap作为前端页面展示。活动列表
当我们登录系统后,看到的第一个页面为活动列表页面:我们直接访问EventAppService来获取活动列表信息。在这里我们需要创建一个angular的控制器:
(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() { eventService.getList(vm.filters).success(function (result) { vm.events = result.items; }); }; 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(); } ]); })();
我们注入EventAppService 服务,在angular 控制器中需要写为:abp.services.app.event。 我们使用ABP的动态webapi方式,他会自动创建webapi服务于angularjs来进行调用。
因此我们在调用应用层方法的时候就会像调用普通的JavaScript 函数一样,因此如果我们要调用C#中的EventAppService.GetList方法,我们在例子中的写法为:eventService.getList 的js函数即可,然后他将返回
一个对象:promise(angular 中为 $q )
关于promise有兴趣的可以访问Promise介绍
我们也可以点击“new event”按钮打开一个新的对话框(模态框,触发vm.openNewEventDialog 函数方法)。这里没有深入讲解关于怎么来操作angular 相关的前端代码
,你可以在代码自己查询研究。
活动详情列表
当我们点击“Details”按钮时,我们会跳转到活动详情页面,比如"http://eventcloud.aspnetboilerplate.com/#/events/e9499e3e-35c0-492c-98ce-7e410461103f".事件的主键为Guid.
在这里,我们可以看到活动的详情信息以及参与的用户。我们可以选参与或者退出该活动。此视图控制器在"Detail.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() { eventService.getDetail({ id: $stateParams.id }).success(function (result) { vm.event = result; }); } 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() { eventService.register({ id: vm.event.id }).success(function (result) { abp.notify.success('Successfully registered to event. Your registration id: ' + result.registrationId + "."); loadEvent(); }); }; vm.cancelRegistertration = function() { eventService.cancelRegistration({ id: vm.event.id }).success(function () { abp.notify.info('Canceled your registration.'); loadEvent(); }); }; vm.cancelEvent = function() { eventService.cancel({ id: vm.event.id }).success(function () { abp.notify.info('Canceled the event.'); vm.backToEventsPage(); }); }; vm.backToEventsPage = function() { $state.go('events'); }; loadEvent(); } ]); })();
这里只展示了event实体服务层的方法,以及操作。
主菜单
顶部菜单栏是由ABP框架动态创建的。我们可以在类”EventCloudNavigationProvider “中定义菜单栏: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" ) ); } }
我们在这里可以添加新的菜单栏。具体可以参考导航文档来阅读。
## angular Route Angular的路由
菜单定义好了之后,只是展示在页面上而已。angular有自己的路由系统。 本次例子是通过Angular ui-router .js来进行路由控制的。他定义在“app.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 }); } ]);
单元测试和集成测试
ABP框架提供了这样的单元测试和集成测试服务工具,它使得测试更加的容易。你可以在你的项目中测试所有的代码。
这里仅仅对基本的测试进行说明。
我们创建EventAppService_Tests 类文件来进行EventAPPService的单元测试:
public class EventAppService_Tests : EventCloudTestBase { private readonly IEventAppService _eventAppService; public EventAppService_Tests() { _eventAppService = Resolve<IEventAppService>(); } [Fact] public async Task Should_Create_Event() { //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] public async Task Should_Not_Create_Events_In_The_Past() { //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); } }
在ABP框架中使用的是xUnit作为测试框架。
在第一个测试中,我们创建了一个活动并且检查了数据库,它是否存在。
在第二次测试中,我们要创建一个过去的活动,当然因为我们的业务上对他进行了限制,他不会创建成功,所以这里会抛出一个异常。
对于单元测试,我们需要测试很多东西,考虑ABP框架本身,以及验证,工作单元等。
社交登录
在ABP生成的模板解决方案中,默认是提供了:Facebook、Google+、Twitter。所以我们只需要在web.config中启用它,并且输入API的凭据即可。<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="" />
具体怎么用请自己百度、bing、google来获取这些凭据信息。
基于令牌(Token)的身份验证
ABP的模板是基于cookie做的身份验证。但是,如果你想通过移动端应用 来进行WEBAPI访问的话,你就需要基于token的身份验证机制了。ABP框架自身包含token的身份认证的基础服务。在Webapi类库中的AccountController类中,就包含了身份验证的方法,然后返回token值的方法(服务)。
然后就可以使用该token进行下一个请求。
在这里我们使用postman 进行演示,他是chrome浏览器的一个插件,用于演示请求和响应。
只是向 http://localhost:6234/api/Account/Authenticate 发送请求,请求类型为json(Context-Type="application/json")
下图所示:
{ "tenancyName":"default", "userNameOrEmailAddress":"admin", "password":"123qwe" }
我们发送了一个Json请求,正文为:
其中包括了tenancyName(租户名称)、userNameOrEmailAddress(用户名)、password(密码)。
相应并且返回的result就是令牌。我们可以将其保存,在下一次的请求中使用。
使用 API
我们在上面的身份授权中,得到了令牌,那么我们就可以用它来做该账户权限范围内的任何事情。所有的应用层的服务都是可以通过远程来调用的。例如,我们可以使用“userservice”来获取用户列表。
图上的为一个POST请求,访问路径:http://localhost:6234/api/services/app/user/GetUsers请求类型依旧为json,内容则为Authorization="Bearer 刚刚得到的令牌内容"。请求的正文为{}。
当然请求不同的API返回的响应正文也会不同嘛。
几乎所有的UI层都可以使用webapi来访问,毕竟UI使用相同的webapi嘛。(and can be consumed easily.)
原文链接:
https://www.codeproject.com/articles/1043326/a-multi-tenant-saas-application-with-asp-net-mvc-a
相关文章推荐
- 基于DDD的.NET开发框架ABP实例,多租户 (Saas)应用程序,采用.NET MVC, Angularjs, EntityFramework-介绍
- 基于DDD的.NET开发框架ABP实例,多租户 (Sass)应用程序,采用.NET MVC, Angularjs, EntityFramework-介绍
- 基于DDD的.NET开发框架ABP实例,多租户 (Sass)应用程序,采用.NET MVC, Angularjs, EntityFramework-介绍
- 基于DDD的.NET开发框架ABP实例,多租户 (Sass)应用程序,采用.NET MVC, Angularjs, EntityFramework-介绍
- 基于DDD的.NET开发框架ABP实例,多租户 (Sass)应用程序,采用.NET MVC, Angularjs, EntityFramework-介绍
- 基于DDD的.NET开发框架ABP实例,多租户 (Sass)应用程序,采用.NET MVC, Angularjs, EntityFramework-介绍
- 基于DDD的.NET开发框架ABP实例,多租户 (Sass)应用程序,采用.NET MVC, Angularjs, EntityFramework-介绍
- 基于DDD的.NET开发框架ABP实例,多租户 (Sass)应用程序,采用.NET MVC, Angularjs, EntityFramework-介绍
- 基于DDD的.NET开发框架 - ABP模块设计
- 基于DDD的.NET开发框架 - ABP依赖注入
- 基于DDD的.NET开发框架 - ABP仓储实现
- 基于DDD的.NET开发框架 - ABP初探
- 基于DDD的.NET开发框架 - ABP工作单元(Unit of Work)
- 线上分享-- 基于DDD的.NET开发框架-ABP介绍
- 基于DDD的.NET开发框架 - ABP日志Logger集成
- 基于DDD的.NET开发框架 - ABP启动配置
- 基于DDD的.NET开发框架 - ABP分层设计
- 基于DDD的.NET开发框架 - ABP分层设计
- 基于DDD的.NET开发框架 - ABP初探
- 基于DDD的.NET开发框架 - ABP领域服务