Search code examples
.netasp.net-coreasp.net-identityclean-architecture

Can an AppUser : IdentityUser get linked to an Employee-entity while keeping separation of concerns with Dependency inversion in Clean architecture


I have chosen to define ApplicationUser class like this in the infrastructure-layer:

namespace MySolution.Infrastructure.Context
{
    // Add profile data for application users by adding properties to the ApplicationUser class
    public class ApplicationUser : IdentityUser
    {
        public EmployeeId EmployeeId { get; set; }
        public Employee Employee { get; set; }
    }

}

I also created a table for connecting a user to an employee

namespace MySolution.Infrastructure.Context
{
    public class EmployeeUserMap
    {
        public string ApplicationUserId { get; set; }
        public ApplicationUser ApplicationUser { get; set; }
        public EmployeeId EmployeeId { get; set; }
        public Employee Employee { get; set; }
    }
}

and here's the entity from my domain layer:


namespace MySolution.Domain.Entities
{
    public class Employee
    {
        //empty constructor
        private Employee() { } // Required by EF core
        
        //properties
        public EmployeeId Id { get; init; }
        public required VatNumber WorkplaceVAT { get; init; }
        public required string EmployeeName { get;  set; }
        public required Address Address { get;  set; }
        public required ContactInfo ContactInfo { get;  set; }
        public required UserRole Role { get; set; }
        public string? ApplicationUserId { get; set; }
        public CompanyId CompanyId { get; init; }

        //Navigational properties
        public Company Company { get; set; }

        [SetsRequiredMembers]
        public Employee(string name, ContactInfo contactInfo, Address address, VatNumber workplaceVAT, UserRole role, CompanyId companyId, string applicationUserId)
        {
            Id = EmployeeId.Create();
            EmployeeName = name ?? throw new ArgumentNullException(nameof(name));
            ContactInfo = contactInfo ?? throw new ArgumentNullException(nameof(contactInfo));
            Address = address ?? throw new ArgumentNullException(nameof(address));
            WorkplaceVAT = workplaceVAT ?? throw new ArgumentNullException(nameof(workplaceVAT));
            Role = role;
            CompanyId = companyId;
            ApplicationUserId = applicationUserId;
            Validate(this);
        }

        private static void Validate(Employee employee)
        {
            var context = new ValidationContext(employee);
            Validator.ValidateObject(employee, context, validateAllProperties : true);
        }
        public void ChangeContactDetails(ContactInfo newContactDetails)
        {
            ContactInfo = newContactDetails ?? throw new ArgumentNullException(nameof(newContactDetails));
        }
        public void ChangeAddress(Address newAddress) 
        { 
            Address = newAddress ?? throw new ArgumentNullException(nameof(newAddress)); 
        }
    }
}

I can't wrap my head around how to keep the domain layer independent but to pass an ApplicationUserId to the entity. I want a one-to-one relationship between Employee-ApplicationUser, and I want to be able to create an Employee first and have the applicationId nullable, and next i want to be able to register a user only if the Employee with same mail exists.

    public async Task RegisterUser(EditContext editContext)
    {
        var employee = await _mediator.Send(new GetEmployeeByEmailQuery(Input.Email), default);

        if (employee != null)
        {
            var user = CreateUser();

            await UserStore.SetUserNameAsync(user, Input.Email, CancellationToken.None);
            var emailStore = GetEmailStore();
            await emailStore.SetEmailAsync(user, Input.Email, CancellationToken.None);

            user.EmployeeId = employee.Id;

            var result = await UserManager.CreateAsync(user, Input.Password);

            if (!result.Succeeded)
            {
                identityErrors = result.Errors;
                return;
            }

            await UserManager.UpdateAsync(user);

            employee.ApplicationUserId = user.Id;

            await _mediator.Send(new UpdateEmployeeCommand(new VatNumber(employee.WorkplaceVat), employee.Name, AddressMapper.ToValueObject(employee.Address), ContactInfoMapper.ToValueObject(employee.ContactInfo), employee.ApplicationUserId!, employee.Role, employee.CompanyId), default);

            Logger.LogInformation("User created a new account with password.");



            var userId = await UserManager.GetUserIdAsync(user);
            var code = await UserManager.GenerateEmailConfirmationTokenAsync(user);
            code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
            var callbackUrl = NavigationManager.GetUriWithQueryParameters(
                NavigationManager.ToAbsoluteUri("Account/ConfirmEmail").AbsoluteUri,
                new Dictionary<string, object?> { ["userId"] = userId, ["code"] = code, ["returnUrl"] = ReturnUrl });

            await EmailSender.SendConfirmationLinkAsync(user, Input.Email, HtmlEncoder.Default.Encode(callbackUrl));

            if (UserManager.Options.SignIn.RequireConfirmedAccount)
            {
                RedirectManager.RedirectTo(
                    "Account/RegisterConfirmation",
                    new() { ["email"] = Input.Email, ["returnUrl"] = ReturnUrl });
            }

            await SignInManager.SignInAsync(user, isPersistent: false);
            RedirectManager.RedirectTo(ReturnUrl);
        }
        else
        {
            identityErrors = new List<IdentityError> { new IdentityError { Description = "Employee not found for the provided email." } };
        }
    }


Solution

  • To maintain domain independence while linking Employee and ApplicationUser you can try this approach:

    1.In Domain Layer Keep Employee clean from infrastructure dependencies. Include a nullable ApplicationUserId and add a method to link it.

    2.In Infrastructure Layer Configure EF Core for a one-to-one relationship in the DbContext.

    3.In Service Layer Handle the registration logic. First, create the Employee, then register the ApplicationUser, link the ApplicationUserId to Employee, and update both entities. This ensures domain integrity and proper linking in the service layer