Open Closed

Update an Entity from the public website #9108


0
marble68 created

Each tenant can create a unit entity called a taker.

Now, the Taker goes to the public website via a unique URL, then enters some info. Once done, I need to update that Taker entity from the public website.

Does disabling filters resolve calling CreateOrEdit? Do I need to impersonate?

Here's what I'm doing. I have this in my Controller for this request:


        [UnitOfWork]
        private void UpdateTaker(CreateOrEditTakerDto input)
        {
           using (var uow = UnitOfWorkManager.Begin())
            {
                using (CurrentUnitOfWork.SetTenantId(null))
                {
                    using (CurrentUnitOfWork.DisableFilter(AbpDataFilters.SoftDelete))
                    {
                        _takersAppService.CreateOrEdit(input).Wait();

                        uow.Complete();
                    }
                }
            }
        }

The error I'm getting is:

AbpAuthorizationException: Current user did not login to the application!

Obviously, the Taker does not log in. How do I do this the "right way". I'm leary of just making a function that can update an entity with no security.

Thanks for any advice.


16 Answer(s)
  • 0
    ismcagdas created
    Support Team

    Hi @marble68,

    Since your AppService requires an authorized user, you can do two things;

    1. Create a domain service and move your code in _takersAppService.CreateOrEdit to this domain service and use it from _takersAppService.CreateOrEdit and your controller in public website. For domain services, you can check https://aspnetboilerplate.com/Pages/Documents/Domain-Services
    2. If you know the user information in the controller in your public website project (UserId and TenantId), you can do this operation on behalf of that user using https://aspnetboilerplate.com/Pages/Documents/Abp-Session#user-identifier.

    In both cases, don't use CurrentUnitOfWork.SetTenantId(null) and CurrentUnitOfWork.DisableFilter(AbpDataFilters.SoftDelete).

    If you want to use option 1, your code will be something like this;

    [UnitOfWork]
    private async Task UpdateTaker(CreateOrEditTakerDto input)
    {
    	using (var uow = UnitOfWorkManager.Begin())
    	{
    		await _takersDomainService.CreateOrEdit(input);
    		await uow.CompleteAsync();
    	}
    }
    
  • 0
    marble68 created

    In the development environment, Im able to get an entity for view without anything, but it seems to not work in production.

    Does this mean I need to do this for getting an entity as well?

    Thanks

  • 0
    musa.demir created
    Support Team

    In the development environment, Im able to get an entity for view without anything, but it seems to not work in production.

    Can you please share the code part?

  • 0
    ismcagdas created
    Support Team

    Hi @marble68,

    Public website and MVC share same cookie names and since they work on localhost for development, probably, public website uses cookie from MVC website to authenticate.

    In production, this is not possible. If you want to use the same approach in production, users must login using login button for public website.

  • 0
    marble68 created

    I see - so, for the public website to be able to retreive entities I should bypass the MVC site altogether?

  • 0
    marble68 created

    Demirmusa - this is how I get the entity in the controller without issue (in development)

    
        public async Task<ActionResult> Index(string id)
            {
                Logger.Debug($"TakeController Index id: {id}");
    
    
                if (Guid.TryParse(id, out Guid _id))
                {
                    Logger.Debug($"TakeController {id} parsed");
                    // We get the taker, determine which tenant
                    EmployeeDto employee = (await _employeesAppService.GetEmployeeForView(_id)).Employee;
                    int TenantId = employee.TenantId;
                    ViewBag.takerId = employee.Id;
    
                    ViewBag.isclear = employee.isclear;
                    if (employee.isclear)
                    {
                        Logger.Debug($"TakeController {id} clear");
                        ViewBag.Takername = employee.firstName + " " + employee.lastName;
                        ViewBag.takerExpires = employee.dateClearExpires;
                        ViewBag.takerDateCleared = employee.dateCleared;
                        ViewBag.takerDateLastSurvey = employee.dateLastSurvey;
    
                    }
                    else
                    {
                        Logger.Debug($"TakeController {id} not clear");
                        // then the active survey for that Tenant
                        GetAllDepartmentsInput input = new GetAllDepartmentsInput();
                        input.activeFilter = 1;
                        input.MaxResultCount = 1;
                        List<GetDepartmentForViewDto> s = _departmentsAppService.GetAll(input).Result.Items.ToList();
                        // No active list - bail.
                        if (s.Count < 1) return RedirectToAction("/");
                        
                        DepartmentDto cs = s.FirstOrDefault().Department;
                        ExpiresInDays = 7;
                        string surveyScriptID = cs.SurveyScriptId.ToString();
                        Logger.Debug($"TakeController {id} survey id {surveyScriptID}");
    
                        if (Guid.TryParse(surveyScriptID, out Guid _csid))
                        {
                            //    // then the survey itself
                            GetSurveyScriptForViewDto surveyScriptDTO = _surveyscriptssAppService.GetSurveyScriptForView(_csid).Result;
                            SurveyScriptDto surveyScript = surveyScriptDTO.SurveyScript;
                            Logger.Debug($"TakeController {id} survey id {surveyScriptID} retrieved");
                            Logger.Debug($"TakeController {id} survey id {surveyScriptID} lenth: {surveyScript.surveyscript.Length}");
    
                            var _surveyScript = surveyScript.surveyscript;
                            SurveyObject = JObject.Parse(_surveyScript);
                            //    // then the json and get that ID - send it down
                            ViewBag.surveyscript = SurveyObject;
                            Logger.Debug($"TakeController {id} survey id {surveyScriptID} Set To Viewbag");
    
                        }
                    }
                    return View();
    
                }
    
                return Redirect("~/");
            }
    
    
  • 0
    marble68 created

    However, in production, it does not get an entity even though the GUID for the entity is valid.

    I added some debug logging and it is successfully pulling the id (guid) and parsing it.

    when it tries to pull the entity by ID (regardless of Tenant) - it fails.

    EmployeeDto employee = (await _employeesAppService.GetEmployeeForView(_id)).Employee;

  • 0
    marble68 created

    ismcagdas - I've reviewed the documentation on creating a domain service.

    Are you saying I should take the code from the controller where I use DTOs, move that to my custom service, and only return the entity?

  • 0
    marble68 created

    I'm trying to create a domain service.

    I created a new application service in Applications project, called EmployeeTakeAppService, and for testing have not put any authentication on the methods.

    I duplicated the IEmployeesTakeAppService and inherit from that for my EmployeeTakeAppService. I don't wish to have a delete method there.

    It still doesn't find the entity in the database.

    So, what I did did not work.

    If I create a domain service - do I put this in the Shared project?

    My goal is, from the TakeController, with the GUID id passed in the URL, get an employee to determine the last time they took a survey. From that, I determine their department. From that, I retreive that Department's employee survey.

    The employee takes the survey.

    The answers are posted back, and the results of the survey are processed, and the Employee entity is updated with when they took the survey.

    If the Employee refreshes the page, they are shown that they've taken the survey and cannot take it again (until the results expire).

    My multi-tenant solution allows companies to put their employees in the database, under their tenant instance, and ask them to complete surveys.

    The URL for the employees is not tenant specific; but it a url generated that is specific to the Employee. All employees, regardless of tenant, are sent to the public website.

    This is why I need to start with the employee to determine the department, to determine which survey to send them.

    Thanks for your help, and I appologize for asking for help with my difficulty in implementing this likely simple thing.

  • 0
    marble68 created

    Attempt to create a domain service, I've added a Manager to the web.core project.

    Here is my EmployeeTakeManager.cs:

    using Abp.Domain.Repositories;
    using okto.work.Survey;
    using System;
    using System.Collections.Generic;
    using System.Text;
    using System.Threading.Tasks;
    
    namespace okto.work.Web.Survey
    {
        public class EmployeeTakeManager : workDomainServiceBase, IEmployeeTakeManager
        {
            private readonly IRepository<Employee, Guid> _employeeRepository;
    
            public EmployeeTakeManager(IRepository<Employee, Guid> employeeRepository)
            {
                _employeeRepository = employeeRepository;
            }
    
            public async Task<Employee> GetEmployeeForTakeAsync(Guid id)
            {
                var employee = await _employeeRepository.GetAsync(id);
                return employee;
            }
    
            Task IEmployeeTakeManager.UpdateEmployeeForTake()
            {
                throw new NotImplementedException();
            }
        }
    }
    
    

    My IEmployeeTakeManager.cs:

    using System;
    using System.Collections.Generic;
    using System.Text;
    using Abp;
    using Abp.Domain.Services;
    using System.Threading.Tasks;
    using okto.work.Survey;
    
    namespace okto.work.Web.Survey
    {
        public interface IEmployeeTakeManager : IDomainService
        {
            Task<Employee> GetEmployeeForTakeAsync(Guid id);
    
            Task UpdateEmployeeForTake();
    
        }
    }
    
    

    My goal is to get a user by ID (Guid) from the repository.

    I've been able to recreate this in dev by adding a localhost file entry for my public site.

    Am I correct that it is not finding the entity because it belongs to Tenant 1, and since I'm requesting it from the public website, there is no tenant? Am I gorrect that using the EmployeeRepository, it is going to check the request to determine the tenant making the request?

    I am attempted to load an entity, regardles of tenant, to provide content for that entity, and update it based on response. Using GUIDs, I'm ensured the IDs are unique (and can accomodate multiple database GUID conflicts if necessary).

    Will a domain service even work?

  • 0
    marble68 created

    The only way I've gotten this to work is by modifying my EmployeeTakeService to expose custom methods, and set Tenant id to null and to clear the filter of MayHaveTenant.

    Only in this circumstance am I able to get to an entity, regardless of Tenant.

    But per your answer, I should not do this, correct?

  • 0
    marble68 created

    I've now made a Take Manager, and from there, call the original service

  • 0
    marble68 created

    My MVC dev environment is pointed at https://mvc. My Public dev environment is pointed at https://public

    I did this to avoid cookie authentication cross contamination.

    In dev, I'm doing this, and it works. However, this does NOT work in production.

    using Abp.Domain.Repositories;
    using okto.work.Survey;
    using System;
    using System.Collections.Generic;
    using System.Text;
    using System.Threading.Tasks;
    using Abp.Domain.Uow;
    using Hangfire.Annotations;
    
    namespace okto.work.Web.Survey
    {
        public class EmployeeTakeManager : workDomainServiceBase, IEmployeeTakeManager
        {
            private readonly IRepository<Employee, Guid> _employeeRepository;
            private readonly IRepository<Department, Guid> _departmentRepository;
            private readonly IRepository<SurveyScript, Guid> _surveryscriptRepository;
    
            public EmployeeTakeManager(
                IRepository<Employee, Guid> employeeRepository, 
                IRepository<Department, Guid> departmentRepository,
                IRepository<SurveyScript, Guid> surveryscriptRepository
                )
            {
                _employeeRepository = employeeRepository;
                _departmentRepository = departmentRepository;
                _surveryscriptRepository = surveryscriptRepository;
            }
    
            [UnitOfWork]
            public async Task<Employee> GetEmployeeForTakeAsync(Guid id)
            {
                Logger.Debug($"TakeManager GetEmployeeForTakeAsync {id}");
    
                Employee employee;
                using (var uow = UnitOfWorkManager.Begin())
                {
                    using (CurrentUnitOfWork.SetTenantId(null))
                    {
                        using (CurrentUnitOfWork.DisableFilter(AbpDataFilters.MayHaveTenant))
                        {
    
                            try
                            {
                                Logger.Debug($"TakeManager _employeeRepository.GetAsync(id) {id}");
    
                                employee = await _employeeRepository.GetAsync(id);
                            }
                            catch (Exception ex)
                            {
    
                                throw ex;
                            }
    
    
                            uow.Complete();
                        }
                    }
                }
                return employee;
            }
        }
    }
    
    

    Connections all work - everything is connecting.

    I have my environment variable set to Production.

    In my TakeController, I'm doing:

                    Employee employee;
                    employee = (await _employeeTakeManager.GetEmployeeForTakeAsync(_id));
    

    Any ideas?

  • 0
    marble68 created

    Based on this: https://support.aspnetzero.com/QA/Questions/9108/Update-an-Entity-from-the-public-website#answer-8ace2a6c-a66e-3758-43c5-39f57cb830ef

    Do I need to, in effect, take any methods I'd call in the normal application service, and move them to this DomainService?

    using only get:

    In an app service, for example, I'd take the method: GetEmployeeForView

    And instead of calling employeeRepository.GetAsync(id); to get an employee entity, i'd instead call employeeDomainService.GetAysnc(id); and return it to the app service from the domain service (referencing the domain service).

    THen, in my controller, I'd reference the domain service, and get my entity directly.

    Correct?

  • 0
    marble68 created

    I think this is working, but not without my setting the TenantID to null and clearing the MayHaveTenant filter.

    I'll keep working with it - but I have something working at the moment. Probably needs refinement.

    I created my DomainService in Core project, right next to the entity.

    I exposed GetAsync, injecting the repo and inheriting from workDomainServiceBase

    I injected the domain service into the Controller, and updated the service in Application to inject the Domain service as well.

    Then, in the GetAsync function in the service in application, I called the getAsync in the domain service.

    In the public controller, I had to create a unit of work method that called the getAync in the domain service, but it would fair if I didn't set TenantID to null and clear the MayHaveTenant filter.

  • 0
    ismcagdas created
    Support Team

    Hi @marble68,

    If the related entity belongs to tenant, you can just switch to host context by setting CurrentUnitOfWork.SetTenantId(null). Disabling filter shouldn't be necesarry in this case.