public async Task <int> Handle(ProvisionTenantCommand request, CancellationToken cancellationToken) { using (var transaction = await context.Database.BeginTransactionAsync()) { var sqlTransaction = transaction.GetDbTransaction(); await context.PaidTimeOffPolicies.AddRangeAsync(new[] { new PaidTimeOffPolicyEntity { TenantId = request.TenantId, EmployeeLevel = 1, Name = "Standard 1", IsDefaultForEmployeeLevel = true, MaxPtoHours = 320.0m, PtoAccrualRate = 10.0m }, new PaidTimeOffPolicyEntity { TenantId = request.TenantId, EmployeeLevel = 2, Name = "Standard 2", IsDefaultForEmployeeLevel = true, MaxPtoHours = 320.0m, PtoAccrualRate = 10.6666m }, new PaidTimeOffPolicyEntity { TenantId = request.TenantId, EmployeeLevel = 3, Name = "Standard 3", IsDefaultForEmployeeLevel = true, MaxPtoHours = 320.0m, PtoAccrualRate = 11.3333m }, new PaidTimeOffPolicyEntity { TenantId = request.TenantId, EmployeeLevel = 4, Name = "Standard 4", IsDefaultForEmployeeLevel = true, MaxPtoHours = 320.0m, PtoAccrualRate = 12.0m }, new PaidTimeOffPolicyEntity { TenantId = request.TenantId, EmployeeLevel = 5, Name = "Standard 5", IsDefaultForEmployeeLevel = true, MaxPtoHours = 320.0m, PtoAccrualRate = 12.6666m }, new PaidTimeOffPolicyEntity { TenantId = request.TenantId, EmployeeLevel = 6, Name = "Standard 6", IsDefaultForEmployeeLevel = true, MaxPtoHours = 320.0m, PtoAccrualRate = 13.3333m }, new PaidTimeOffPolicyEntity { TenantId = request.TenantId, EmployeeLevel = 7, Name = "Standard 7", IsDefaultForEmployeeLevel = true, MaxPtoHours = 320.0m, PtoAccrualRate = 14.0m }, new PaidTimeOffPolicyEntity { TenantId = request.TenantId, EmployeeLevel = 8, Name = "Standard 8", IsDefaultForEmployeeLevel = true, MaxPtoHours = 320.0m, PtoAccrualRate = 14.6666m }, new PaidTimeOffPolicyEntity { TenantId = request.TenantId, EmployeeLevel = 8, Name = "Unlimited 8", IsDefaultForEmployeeLevel = false, AllowsUnlimitedPto = true }, new PaidTimeOffPolicyEntity { TenantId = request.TenantId, EmployeeLevel = 9, Name = "Standard 9", IsDefaultForEmployeeLevel = true, MaxPtoHours = 320.0m, PtoAccrualRate = 15.3333m }, new PaidTimeOffPolicyEntity { TenantId = request.TenantId, EmployeeLevel = 9, Name = "Unlimited 9", IsDefaultForEmployeeLevel = false, AllowsUnlimitedPto = true }, new PaidTimeOffPolicyEntity { TenantId = request.TenantId, EmployeeLevel = 10, Name = "Standard 10", IsDefaultForEmployeeLevel = true, MaxPtoHours = 320.0m, PtoAccrualRate = 16.0m }, new PaidTimeOffPolicyEntity { TenantId = request.TenantId, EmployeeLevel = 10, Name = "Unlimited 10", IsDefaultForEmployeeLevel = false, AllowsUnlimitedPto = true }, }); await context.SaveChangesAsync(); await transaction.CommitAsync(); return(request.TenantId); } }
public async Task SeedAllAsync(CancellationToken cancellationToken) { if (context.PaidTimeOffPolicies.Any()) { logger.LogInformation("Database has already been initialized with basic data. Nothing to do."); return; } await SeedPtoPoliciesAsync(); await SeedCurrenciesAsync(); await context.SaveChangesAsync(cancellationToken); }
private async Task CreateDefaultTenantsAsync(DbTransaction sqlTransaction, CustomerEntity customer, IEnumerable <TenantViewModel> tenants) { await facade.TurnOffIdentityIncrementAsync(nameof(IApplicationWriteDbContext.Tenants), sqlTransaction); foreach (var tenant in tenants.Select(t => tenantViewModelToDbEntityMapper.Map(t))) { tenant.CustomerId = customer.Id; await context.Tenants.AddAsync(tenant); await context.TenantAspNetUsers.AddAsync(new TenantAspNetUserEntity { AspNetUsersId = customer.AspNetUsersId, TenantId = tenant.Id }); } await context.SaveChangesAsync(); await facade.TurnOnIdentityIncrementAsync(nameof(IApplicationWriteDbContext.Tenants), sqlTransaction); }
public async Task <TenantViewModel> Handle(AddOrUpdateTenantCommand request, CancellationToken cancellationToken) { var isNew = false; // PRESENTATION/APPLICATION LAYER var tenantViewModel = request.Tenant; // PERSISTENCE LAYER using (var transaction = await context.Database.BeginTransactionAsync()) { var sqlTransaction = transaction.GetDbTransaction(); TenantEntity tenantEntity = null; // Check for existing slug/key on a different tenant. var slug = tenantViewModel.Slug.Sluggify(); var id = await facade.QueryFirstOrDefaultAsync <int?>( @"SELECT TOP 1 Id FROM Tenants WITH(NOLOCK) WHERE Slug = @slug AND (@Id IS NULL OR Id <> @Id)", new { slug, tenantViewModel.Id }, sqlTransaction); if (id != null) { throw new ApplicationLayerException("Tenant slug already in use by another tenant."); } else { tenantViewModel.Slug = slug; } // Update. if (tenantViewModel.Id != null) { var tenantId = (int)tenantViewModel.Id; tenantEntity = context.Tenants.Find(tenantId); if (tenantEntity == null) { throw new ApplicationLayerException($"Tenant with ID {tenantId} is in the customer's access list, but does not seem to exist."); } mapper.Map(tenantViewModel, tenantEntity); } else { // Add. var customerId = await facade.QueryFirstOrDefaultAsync <int>("SELECT TOP 1 Id FROM Customers WHERE AspNetUsersId = @AspNetUsersId", request, sqlTransaction); tenantEntity = mapper.Map(tenantViewModel); tenantEntity.CustomerId = customerId; await context.Tenants.AddAsync(tenantEntity); isNew = true; } await context.SaveChangesAsync(); tenantViewModel.Id = tenantEntity.Id; if (isNew) { await context.TenantAspNetUsers.AddAsync(new TenantAspNetUserEntity { AspNetUsersId = request.AspNetUsersId, TenantId = tenantEntity.Id }); await context.SaveChangesAsync(); } await transaction.CommitAsync(); } return(tenantViewModel); }
public async Task <RegisterOrUpdateEmployeeCommand> Handle(RegisterOrUpdateEmployeeCommand request, CancellationToken cancellationToken) { // Map the view model to a domain entity. We are now in the realm of business entities and logic. var employee = employeeVmToDomainEntityMapper.MapToDomainEntity(request); var isNewEmployee = employee.Id == null; // Look up manager from DB. EmployeeEntity managerEntity = null; if (request.ManagerId != null) { managerEntity = await context.Employees.FindAsync(request.ManagerId); if (managerEntity == null) { throw new EmployeeException($"Manager with ID {request.ManagerId} not found."); } // Convert to domain entity as well. employee = employee.WithManager(employeeDomainToDbEntityMapper.MapToDomainEntity(managerEntity)); } // Look up subordinates and map those to domain entities. var subordinateIds = request.SubordinateIds.ToArray(); var subordinateEntities = await(from e in context.Employees.AsNoTracking() where subordinateIds.Contains(e.Id) select e).ToArrayAsync(); employee = employee.WithSubordinates(from e in subordinateEntities select employeeDomainToDbEntityMapper.MapToDomainEntity(e)); // Get PTO policy for this employee and map to domain entity. var policyEntity = await context.PaidTimeOffPolicies.AsNoTracking().FirstOrDefaultAsync(p => p.Id == request.PaidTimeOffPolicyId); if (policyEntity == null) { throw new EmployeeException($"PTO policy not found."); } var policy = ptoPolicyDomainToDbEntityMapper.MapToDomainEntity(policyEntity); employee = employee.WithPaidTimeOffPolicy(policy); // Check the validity of the employee's PTO hours and other business logic. All business rule validation is still happening within the Domain layer... employee.AssertAggregates(); employee.VerifyEmployeeManagerAndSubordinates(); if (isNewEmployee) { employee.VerifyStartingPtoHoursAreValid(); } else { employee.VerifyPtoHoursAreValid(); } // End business logic. // Convert back to persistence entity. var employeeEntity = employeeDomainToDbEntityMapper.MapToDbEntity(employee); employeeEntity.TenantId = TenantId; var entry = await context.Employees.AddAsync(employeeEntity); if (!isNewEmployee) { entry.State = EntityState.Modified; } // Delete previous entity relationships. context.EmployeeManagers.RemoveRange(context.EmployeeManagers.Where(em => em.EmployeeId == employee.Id)); context.EmployeeManagers.RemoveRange(context.EmployeeManagers.Where(em => em.ManagerId == employee.Id)); // Commit transaction to DB. await context.SaveChangesAsync(cancellationToken); // Create new relationships. if (employee.Manager != null) { await context.EmployeeManagers.AddAsync(new EmployeeManagerEntity { TenantId = TenantId, EmployeeId = employeeEntity.Id, ManagerId = managerEntity.Id }); } await context.EmployeeManagers.AddRangeAsync(from s in subordinateEntities select new EmployeeManagerEntity { TenantId = TenantId, EmployeeId = s.Id, ManagerId = employeeEntity.Id }); // Commit transaction to DB. if (context.HasChanges) { await context.SaveChangesAsync(cancellationToken); } // Set ID of domain entity. employee.Id = employeeEntity.Id; // Kick off domain events. employee.CreateEmployeeRegisteredEvent(dateTimeService); await employee.DispatchDomainEventsAsync(); // Map from domain entity back to VM (command) and return that. return(employeeVmToDomainEntityMapper.MapToViewModel(employee)); }
public async Task <SubmitNewPaidTimeOffRequestViewModel> Handle(SubmitNewPaidTimeOffRequestCommand request, CancellationToken cancellationToken) { var today = DateTime.Today; // PRESENTATION/APPLICATION LAYER var timeOffRequestViewModel = request.PaidTimeOffRequest; timeOffRequestViewModel.StartDate = timeOffRequestViewModel.StartDate.Date; timeOffRequestViewModel.EndDate = timeOffRequestViewModel.EndDate.Date; // PERSISTENCE LAYER var submittedByEntity = await facade.QueryFirstOrDefaultAsync <EmployeeEntity>(@"SELECT TOP 1 * FROM Employees WITH(NOLOCK) WHERE AspNetUsersId = @AspNetUsersId AND TenantId = @TenantId", new { request.AspNetUsersId, request.PaidTimeOffRequest.TenantId }); var forEmployeeEntity = timeOffRequestViewModel.ForEmployeeId == null? context.Employees.Include(e => e.PaidTimeOffPolicy).Include(e => e.ForPaidTimeOffRequests).FirstOrDefault(e => e.AspNetUsersId == request.AspNetUsersId && e.TenantId == request.PaidTimeOffRequest.TenantId) : context.Employees.Include(e => e.PaidTimeOffPolicy).Include(e => e.ForPaidTimeOffRequests).FirstOrDefault(e => e.Id == timeOffRequestViewModel.ForEmployeeId && e.TenantId == request.PaidTimeOffRequest.TenantId) ; // TODO: if submitting on behalf of another employee, use the domain layer to validate that they have the privileges to do so. if ( forEmployeeEntity == null || submittedByEntity == null) { throw new ApplicationLayerException(@"Unable to query employees while trying to create time off request aggregate."); } // DOMAIN LAYER var forEmployee = mapper.MapDbEntityToDomainEntity <EmployeeEntity, Employee>(forEmployeeEntity); var submittedByEmployee = mapper.MapDbEntityToDomainEntity <EmployeeEntity, Employee>(submittedByEntity); var paidTimeOffPolicy = mapper.MapDbEntityToDomainEntity <PaidTimeOffPolicyEntity, PaidTimeOffPolicy>(forEmployeeEntity.PaidTimeOffPolicy); var existingRequests = (from req in forEmployeeEntity.ForPaidTimeOffRequests select mapper.MapDbEntityToDomainEntity <PaidTimeOffRequestEntity, PaidTimeOffRequest>(req)).ToList(); // Build up the Domain aggregate entity so that complex business logic can be executed against it. In an enterprise (non-demo) solution this // would likely involve special rules involving accrued hours, whether the company allows going negative in PTO hours, managerial overrides, // etc. The point is that the entities, and the logic which operates against them, are separate from view models and database persistence models. var submittedRequest = mapper.MapViewModelToDomainEntity <SubmitNewPaidTimeOffRequestViewModel, PaidTimeOffRequest>(request.PaidTimeOffRequest) .WithForEmployee(forEmployee) .WithSubmittedBy(submittedByEmployee) .WithPaidTimeOffPolicy(paidTimeOffPolicy); // Ensure the aggregate is in a valid state with which to perform business logic. submittedRequest.ValidateAggregate(); // Perform basic validation against the request to make sure that the employee is OK to submit this paid time off request. Once again, the logic // inside the service is naive and overly-simplistic. A real-life solution would be much more involved. var validationResult = paidTimeOffRequestService.ValidatePaidTimeOffRequest(submittedRequest, existingRequests, paidTimeOffPolicy, today); timeOffRequestViewModel.Result = validationResult; if (validationResult != PaidTimeOffRequestValidationResult.OK) { return(timeOffRequestViewModel); } // Submit the time off request (changes Domain state and creates event). submittedRequest = submittedRequest.Submit(); // PERSISTENCE LAYER var paidTimeOffRequestEntity = mapper.MapDomainEntityToDbEntity <PaidTimeOffRequest, PaidTimeOffRequestEntity>(submittedRequest); paidTimeOffRequestEntity.TenantId = timeOffRequestViewModel.TenantId; await context.PaidTimeOffRequests.AddAsync(paidTimeOffRequestEntity, cancellationToken); await context.SaveChangesAsync(cancellationToken); // PRESENTATION LAYER request.PaidTimeOffRequest.CreatedPaidTimeOffRequest = mapper.MapDomainEntityToViewModel <PaidTimeOffRequest, PaidTimeOffRequestViewModel>(submittedRequest); // DISPATCH DOMAIN EVENTS await submittedRequest.DispatchDomainEventsAsync(); return(request.PaidTimeOffRequest); }