zoukankan      html  css  js  c++  java
  • 基于DDD的.NET开发框架ABP实例,多租户 (Saas)应用程序,采用.NET MVC, Angularjs, EntityFrame-EventCloud

    活动云项目

    在本文中,我们将展示本项目的关键部分并且给予注释信息和说明。建议从网站模板中输入“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作为前端页面展示。

    活动列表

    当我们登录系统后,看到的第一个页面为活动列表页面:
    image

    我们直接访问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.

    image

    在这里,我们可以看到活动的详情信息以及参与的用户。我们可以选参与或者退出该活动。此视图控制器在"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")
    下图所示:

    image

    {
    "tenancyName":"default",
    "userNameOrEmailAddress":"admin",
    "password":"123qwe"
    
    }
    
    

    我们发送了一个Json请求,正文为:
    其中包括了tenancyName(租户名称)、userNameOrEmailAddress(用户名)、password(密码)。
    相应并且返回的result就是令牌。我们可以将其保存,在下一次的请求中使用。

    使用 API

    我们在上面的身份授权中,得到了令牌,那么我们就可以用它来做该账户权限范围内的任何事情。所有的应用层的服务都是可以通过远程来调用的。
    例如,我们可以使用“userservice”来获取用户列表。
    image

    图上的为一个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

    为了方便和大家交流我建立了几个群,欢迎大家加群交流哦~


    作者:梁桐铭52ABP:基于DDD强大稳定的WEB应用框架!
    出处:http://www.cnblogs.com/wer-ltm
    作品角落的白板报 创作,采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
    欢迎转载,但任何转载必须保留完整文章,在显要地方显示署名以及原文链接。如您有任何疑问或者授权方面的协商,请 加群沟通留言

  • 相关阅读:
    alias这个命令还是很有用的
    为什么不推荐用破解版的winrar
    chrome headless
    关于PDF的一些书籍
    PDF的一些工具
    3DPDF是个什么东西?
    你可能不知道的pdf的功能
    为什么一些公司把dwg文件转化为pdf
    关于pdf阅读器的选择
    接外包怎么保护自己的作品
  • 原文地址:https://www.cnblogs.com/wer-ltm/p/6419145.html
Copyright © 2011-2022 走看看