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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM