基于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

posted @ 2017-02-20 13:40  梁桐铭  阅读(4741)  评论(0编辑  收藏  举报