We did not add the mapping in CustomDtoMapper, as we are bypassing the use of automapper altogether. We use the follow code to map entities to Dto:
public abstract class ViewEntityModel<TEntity, TViewModel, TPrimaryKey> : EntityDto<TPrimaryKey>, IViewModel<TEntity> where TEntity : class
where TViewModel : class, IViewModel<TEntity>, new()
{
public static TViewModel CreateFrom(TEntity entity)
{
var vm = new TViewModel();
vm.MapFrom(entity);
return vm;
}
public abstract void MapFrom(TEntity entity);
}
Is it possible to take advantage of GraphQL while doing custom mapping, or are we forced to use Automapper? Thanks.
We're experimenting with adding GraphQL into our project. We have created types for our complex objects, but now we're getting an error when trying to run the query in the playground. From the message, it appears that GraphQL expects automapper to map Dto to domain objects, however, we are mapping Dto with our own custom code, bypassing automapper.
Is it possbile to tell the GraphQL query to map using our own custom code, instead of automapper?
Here is the full error message/stack trace that comes on the query:
"GraphQL.ExecutionError: Error trying to resolve Entries.\r\n ---> System.InvalidOperationException: Missing map from MyProject.Domain.TimeTracking.TimeEntry to MyProject.Domain.Core.TimeEntries.Dto.TimeEntryDto. Create using CreateMap<TimeEntry, TimeEntryDto>.\r\n at AutoMapper.QueryableExtensions.ExpressionBuilder.CreateMapExpressionCore(ExpressionRequest request, Expression instanceParameter, IDictionary
2 typePairCount, LetPropertyMaps letPropertyMaps, TypeMap& typeMap)\r\n at AutoMapper.QueryableExtensions.ExpressionBuilder.CreateMapExpression(ExpressionRequest request, IDictionary
2 typePairCount, LetPropertyMaps letPropertyMaps)\r\n at AutoMapper.QueryableExtensions.ExpressionBuilder.CreateMapExpression(ExpressionRequest request)\r\n at AutoMapper.LockingConcurrentDictionary2.<>c__DisplayClass2_1.<.ctor>b__1()\r\n at System.Lazy
1.ViaFactory(LazyThreadSafetyMode mode)\r\n at System.Lazy1.ExecutionAndPublication(LazyHelper executionAndPublication, Boolean useDefaultConstructor)\r\n at System.Lazy
1.CreateValue()\r\n at AutoMapper.LockingConcurrentDictionary2.GetOrAdd(TKey key)\r\n at AutoMapper.QueryableExtensions.ExpressionBuilder.GetMapExpression(Type sourceType, Type destinationType, Object parameters, MemberInfo[] membersToExpand)\r\n at AutoMapper.QueryableExtensions.ProjectionExpression.ToCore[TResult](Object parameters, IEnumerable
1 memberPathsToExpand)\r\n at AutoMapper.QueryableExtensions.ProjectionExpression.To[TResult](Object parameters, Expression1[] membersToExpand)\r\n at AutoMapper.QueryableExtensions.Extensions.ProjectTo[TDestination](IQueryable source, IConfigurationProvider configuration, Object parameters, Expression
1[] membersToExpand)\r\n at AutoMapper.Mapper.ProjectTo[TDestination](IQueryable source, Object parameters, Expression1[] membersToExpand)\r\n at MyProject.Core.Base.MyProjectQueryBase
2.ProjectTo[TDestination](IQueryable source) in C:\Users\shodg\MyProject\src\MyProject.GraphQL\Core\Base\MyProjectQueryBase.cs:line 65\r\n at MyProject.Core.Base.MyProjectQueryBase2.ProjectToListAsync[TDestination](IQueryable source) in C:\\Users\\shodg\\MyProject\\src\\MyProject.GraphQL\\Core\\Base\\MyProjectQueryBase.cs:line 71\r\n at MyProject.Queries.TimeEntryQuery.Resolve(ResolveFieldContext
1 context) in C:\Users\shodg\MyProject\src\MyProject.GraphQL\Queries\TimeEntryQuery.cs:line 60\r\n at MyProject.Core.Base.MyProjectQueryBase2.InternalResolve(ResolveFieldContext
1 context) in C:\Users\shodg\MyProject\src\MyProject.GraphQL\Core\Base\MyProjectQueryBase.cs:line 47\r\n at Abp.Threading.InternalAsyncHelper.AwaitTaskWithPostActionAndFinallyAndGetResult[T](Task1 actualReturnValue, Func
1 postAction, Action`1 finalAction)\r\n at GraphQL.Instrumentation.MiddlewareResolver.Resolve(ResolveFieldContext context)\r\n at GraphQL.Execution.ExecutionStrategy.ExecuteNodeAsync(ExecutionContext context, ExecutionNode node)\r\n --- End of inner exception stack trace ---"
I've read that documentation. What it doesn't provide is guidance around how to build dependencies that themselves depend on repositories if we want to leverage ABP's implicit UoW management. Are there best practices to follow around this beyond "don't use the repo in construcors"?
It is not reasonable, if even possible, to expect ABP or ASP.NET Zero to create a DB connection for every class that you inject.
There's clearly a different paradigm within ABP/A0 than with most other frameworks. Typically I wouldn't expect an IoC container to inject dependencies that aren't ready for use, but that seems to be the case here due to the implicit UoW management that ABP provides. The fact that this works under some circumstances but not others adds to the confusion - eg, removing the intermediate call to GetAllIncluding()
and instead using onlyFirstOrDefault()
results in a successful DB query via the repository.
I've read the UoW, DI, and Repository documentation for ABP, but I'm not seeing code examples or guidance on best practices are around how to implement a dependency that has its own dependency on a repository or dbcontext. Can you point me towards detailed documention or examples on how this should be done? Additional guidance would be appreciated.
Please elaborate. I'm not sure how expecting fully wired up dependencies to be injected into my own dependency injected class regardless of timing and calling safe methods within said dependency should require me to manage my own UoW.
This is a strange one, and I'm not sure if the problem lies with A0, ABP, Windsor, or some combination of the 3. We're injecting a dependency (D1) into another dependency (D2) into an application service. Both D1 and D2 implement ITransientDependency. D1 takes an IRepository via constructor injection. Here's an arbitrary code snippet of D1 to illustrate:
public class StatusProvider : IStatusProvider, ITransientDependency
{
private readonly StatusForStaffMember _statusForStaffMember;
public StatusProvider(IRepository<StatusForStaffMember> statusRepo, IRepository<StaffMember> staffMemberRepo, IAbpSession abpSession)
{
_statusForStaffMember = statusRepo.GetAllIncluding(s => s.User).FirstOrDefault(s => s.User.Id == abpSession.GetUserId()); //I'm gonna throw right here
if (_statusForStaffMember == null)
{
var staffMember = staffMemberRepo.Get(abpSession.GetUserId());
_statusForStaffMember = new StatusForStaffMember(staffMember);
}
}
public StaffMember GetCurrentStaffMember()
{
return _statusForStaffMember.User;
}
}
This explodes with a big nasty exception when statusRepo is used, but it boils down to this:
System.ObjectDisposedException: 'Cannot access a disposed object. A common cause of this error is disposing a context that was resolved from dependency injection and then later trying to use the same context instance elsewhere in your application. This may occur if you are calling Dispose() on the context, or wrapping the context in a using statement. If you are using dependency injection, you should let the dependency injection container take care of disposing context instances.
This seems odd, as we were just handed the repo. Why has something already been disposed within it when we haven't even made it into our ApplicationService yet? I have no idea, yet if we refactor the above to lazily grab our _statusForStaffMember
from the repo like the following, everything works fine:
public class StatusProvider : IStatusProvider, ITransientDependency
{
private readonly Func<StatusForStaffMember> _statusForStaffMember;
public StatusProvider(IRepository<StatusForStaffMember> statusRepo, IRepository<StaffMember> staffMemberRepo, IAbpSession abpSession)
{
StatusForStaffMember closedOver;
_statusForStaffMember = () =>
{
closedOver = _statusRepo.GetAllIncluding(s => s.User).FirstOrDefault(t => t.User.Id == abpSession.GetUserId()); //everything is fine now
if (closedOver == null)
{
var staffMember = staffMemberRepo.Get(abpSession.GetUserId());
closedOver = new StatusForStaffMember(staffMember);
}
return closedOver;
};
}
public StaffMember GetCurrentStaffMember()
{
return _statusForStaffMember().User;
}
}
I do NOT understand why I'm getting the ObjectDisposedException if I access the repo early but not if I access it late. If I dig into the repo at runtime via ProxyUtil.GetUnproxiedInstance(_statusRepo)
I get ArgumentNullExceptions on Connection, Context, DbQueryTable, Table, and Transaction. The only properties that are properly instantiated appear to be IocResolver and UnitOfWorkManager. If I were going to venture to guess, I'd say that the repo is using Property Injection, and it's being handed to our dependency via Constructor Injection before its fully wired up and that the ObjectDisposedException is misleading.
Please help me understand what is happening, why, and how to resolve it without a using a lazy loading approach.
While our original issue has been fixed in the upgrade to 8.1, we now have another bug when adding new migrations. Here are the steps to reproduce this issue on a clean demo project of v.8.1:
The migration adds the following column:
migrationBuilder.AddColumn<int>(
name: "EditionPaymentType",
table: "AppSubscriptionPayments",
nullable: false,
defaultValue: 0);
Because of this column, running update-database produces the following exception:
Column names in each table must be unique. Column name 'EditionPaymentType' in table 'AppSubscriptionPayments' is specified more than once.
Here is the Stack Trace for the exception:
Applying migration '20200102200758_Add_User_With_Address'.
Failed executing DbCommand (7ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
ALTER TABLE [AppSubscriptionPayments] ADD [EditionPaymentType] int NOT NULL DEFAULT 0;
Microsoft.Data.SqlClient.SqlException (0x80131904): Column names in each table must be unique. Column name 'EditionPaymentType' in table 'AppSubscriptionPayments' is specified more than once.
at Microsoft.Data.SqlClient.SqlConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)
at Microsoft.Data.SqlClient.SqlInternalConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)
at Microsoft.Data.SqlClient.TdsParser.ThrowExceptionAndWarning(TdsParserStateObject stateObj, Boolean callerHasConnectionLock, Boolean asyncClose)
at Microsoft.Data.SqlClient.TdsParser.TryRun(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataReader dataStream, BulkCopySimpleResultSet bulkCopyHandler, TdsParserStateObject stateObj, Boolean& dataReady)
at Microsoft.Data.SqlClient.SqlCommand.RunExecuteNonQueryTds(String methodName, Boolean isAsync, Int32 timeout, Boolean asyncWrite)
at Microsoft.Data.SqlClient.SqlCommand.InternalExecuteNonQuery(TaskCompletionSource`1 completion, Boolean sendToPipe, Int32 timeout, Boolean& usedCache, Boolean asyncWrite, Boolean inRetry, String methodName)
at Microsoft.Data.SqlClient.SqlCommand.ExecuteNonQuery()
at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteNonQuery(RelationalCommandParameterObject parameterObject)
at Microsoft.EntityFrameworkCore.Migrations.MigrationCommand.ExecuteNonQuery(IRelationalConnection connection, IReadOnlyDictionary`2 parameterValues)
at Microsoft.EntityFrameworkCore.Migrations.Internal.MigrationCommandExecutor.ExecuteNonQuery(IEnumerable`1 migrationCommands, IRelationalConnection connection)
at Microsoft.EntityFrameworkCore.Migrations.Internal.Migrator.Migrate(String targetMigration)
at Microsoft.EntityFrameworkCore.Design.Internal.MigrationsOperations.UpdateDatabase(String targetMigration, String contextType)
at Microsoft.EntityFrameworkCore.Design.OperationExecutor.UpdateDatabaseImpl(String targetMigration, String contextType)
at Microsoft.EntityFrameworkCore.Design.OperationExecutor.UpdateDatabase.<>c__DisplayClass0_0.<.ctor>b__0()
at Microsoft.EntityFrameworkCore.Design.OperationExecutor.OperationBase.Execute(Action action)
ClientConnectionId:359e964b-b07a-4130-856e-db417d8e28ec
Error Number:2705,State:4,Class:16
Thanks for your response. Unfortunately, upgrading to 5.1 did not fix the issue.
Following the Github issue you linked, we have disabled entity history, and that works as a temporary workaround. But please let us know if/when there is a more stable solution.
We have created a domain entity StaffMember
that inherits from User
. Creating/editing staff members was working, but when adding an owned type, Address
, onto the entity, we get a null reference exception when trying to create a new staff member with an address.
Here is the exception and stack trace, from the logs:
ERROR 2019-12-19 16:47:59,889 [33 ] Mvc.ExceptionHandling.AbpExceptionFilter - Object reference not set to an instance of an object.
System.NullReferenceException: Object reference not set to an instance of an object.
at Abp.EntityHistory.EntityHistoryHelper.CreateEntityChangeSet(ICollection`1 entityEntries)
at Abp.Zero.EntityFrameworkCore.AbpZeroCommonDbContext`3.SaveChangesAsync(CancellationToken cancellationToken)
at Abp.EntityFrameworkCore.Uow.EfCoreUnitOfWork.SaveChangesInDbContextAsync(DbContext dbContext)
at Abp.EntityFrameworkCore.Uow.EfCoreUnitOfWork.SaveChangesAsync()
at Abp.Authorization.Users.AbpUserStore`2.CreateAsync(TUser user, CancellationToken cancellationToken)
at Microsoft.AspNetCore.Identity.UserManager`1.CreateAsync(TUser user)
at Abp.Authorization.Users.AbpUserManager`2.CreateAsync(TUser user)
at MyApp.Authorization.Users.StaffMemberAppService.CreateStaffMemberAsync(CreateOrUpdateStaffMemberInput input) in C:\Users\shodg\myapp\src\MyApp.Application\Domain\Core\StaffMembers\StaffMemberAppService.cs:line 290
at Abp.Threading.InternalAsyncHelper.AwaitTaskWithPostActionAndFinally(Task actualReturnValue, Func`1 postAction, Action`1 finalAction)
at MyApp.Authorization.Users.StaffMemberAppService.CreateOrUpdateStaffMember(CreateOrUpdateStaffMemberInput input) in C:\Users\shodg\myapp\src\MyApp.Application\Domain\Core\StaffMembers\StaffMemberAppService.cs:line 203
at Abp.Threading.InternalAsyncHelper.AwaitTaskWithPostActionAndFinally(Task actualReturnValue, Func`1 postAction, Action`1 finalAction)
at lambda_method(Closure , Object )
at Microsoft.Extensions.Internal.ObjectMethodExecutorAwaitable.Awaiter.GetResult()
at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.AwaitableResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeActionMethodAsync>g__Awaited|12_0(ControllerActionInvoker invoker, ValueTask`1 actionResultValueTask)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeNextActionFilterAsync>g__Awaited|10_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeInnerFilterAsync>g__Awaited|13_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextExceptionFilterAsync>g__Awaited|25_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
And here is the method in our service that creates staff members:
[AbpAuthorize(AppPermissions.Pages_Administration_StaffMembers_Create)]
protected virtual async System.Threading.Tasks.Task CreateStaffMemberAsync(CreateOrUpdateStaffMemberInput input)
{
if (AbpSession.TenantId.HasValue)
{
await _userPolicy.CheckMaxUserCountAsync(AbpSession.GetTenantId());
}
var staffMember = ObjectMapper.Map<StaffMember>(input.User); //Passwords is not mapped (see mapping configuration)
staffMember.TenantId = AbpSession.TenantId;
//Set password
if (input.SetRandomPassword)
{
var randomPassword = await _userManager.CreateRandomPassword();
staffMember.Password = _passwordHasher.HashPassword(staffMember, randomPassword);
input.User.Password = randomPassword;
}
else if (!input.User.Password.IsNullOrEmpty())
{
await UserManager.InitializeOptionsAsync(AbpSession.TenantId);
foreach (var validator in _passwordValidators)
{
CheckErrors(await validator.ValidateAsync(UserManager, staffMember, input.User.Password));
}
staffMember.Password = _passwordHasher.HashPassword(staffMember, input.User.Password);
}
staffMember.ShouldChangePasswordOnNextLogin = input.User.ShouldChangePasswordOnNextLogin;
//TODO: replace US's countryID (220) with functionality to load states dynamically based on country selection
await staffMember.Address.SetCountryAndState(_countryRepository, _stateRepository, 220, input.User.Address.StateId);
//Assign roles
staffMember.Roles = new Collection<UserRole>();
foreach (var roleName in input.AssignedRoleNames)
{
var role = await _roleManager.GetRoleByNameAsync(roleName);
staffMember.Roles.Add(new UserRole(AbpSession.TenantId, staffMember.Id, role.Id));
}
CheckErrors(await UserManager.CreateAsync(staffMember));
await CurrentUnitOfWork.SaveChangesAsync(); //To get new user's Id.
//Notifications
await _notificationSubscriptionManager.SubscribeToAllAvailableNotificationsAsync(staffMember.ToUserIdentifier());
await _appNotifier.WelcomeToTheApplicationAsync(staffMember);
//Organization Units
await UserManager.SetOrganizationUnitsAsync(staffMember, input.OrganizationUnits.ToArray());
//Send activation email
if (input.SendActivationEmail)
{
staffMember.SetNewEmailConfirmationCode();
await _userEmailer.SendEmailActivationLinkAsync(
staffMember,
AppUrlService.CreateEmailActivationUrlFormat(AbpSession.TenantId),
input.User.Password
);
}
}
The exception is thrown at CheckErrors(await UserManager.CreateAsync(staffMember))
.
If helpful, here is the modelBuilder mapping for StaffMember class from our Db context:
modelBuilder.Entity<StaffMember>(sm =>
{
sm.HasIndex(e => new { e.TenantId });
sm.HasMany(typeof(Project), nameof(StaffMember.Projects));
sm.OwnsOne(e => e.Address);
});
Is there a workaround/solution to this behavior? Let us know if you need additional info, and we can provide.
Thank you for the direction. Unfortunately edition is a relatively simple compared to User where there is substantially more process around it.
I have marked below in the original User creation method from UserAppService.cs at the start of every block that will be duplicated in a create method on StaffMember with ///DUPLICATED
to indicate the level of duplication im concerned about.
[AbpAuthorize(AppPermissions.Pages_Administration_Users_Create)]
protected virtual async Task CreateUserAsync(CreateOrUpdateUserInput<UserEditDto> input)
{
///DUPLICATED
if (AbpSession.TenantId.HasValue)
{
await _userPolicy.CheckMaxUserCountAsync(AbpSession.GetTenantId());
}
/// This will be different because we have to use custom mappings
var user = ObjectMapper.Map<User>(input.User); //Passwords is not mapped (see mapping configuration)
user.TenantId = AbpSession.TenantId;
///DUPLICATED
//Set password
if (input.SetRandomPassword)
{
var randomPassword = await _userManager.CreateRandomPassword();
user.Password = _passwordHasher.HashPassword(user, randomPassword);
input.User.Password = randomPassword;
}
else if (!input.User.Password.IsNullOrEmpty())
{
await UserManager.InitializeOptionsAsync(AbpSession.TenantId);
foreach (var validator in _passwordValidators)
{
CheckErrors(await validator.ValidateAsync(UserManager, user, input.User.Password));
}
user.Password = _passwordHasher.HashPassword(user, input.User.Password);
}
///DUPLICATED
user.ShouldChangePasswordOnNextLogin = input.User.ShouldChangePasswordOnNextLogin;
///DUPLICATED (with minor changes if our derived users end up having dedicated roles)
//Assign roles
user.Roles = new Collection<UserRole>();
foreach (var roleName in input.AssignedRoleNames)
{
var role = await _roleManager.GetRoleByNameAsync(roleName);
user.Roles.Add(new UserRole(AbpSession.TenantId, user.Id, role.Id));
}
///DUPLICATED
CheckErrors(await UserManager.CreateAsync(user));
await CurrentUnitOfWork.SaveChangesAsync(); //To get new user's Id.
///DUPLICATED (I can see a reason this wouldnt be duplicated for some subclassed users)
//Notifications
await _notificationSubscriptionManager.SubscribeToAllAvailableNotificationsAsync(user.ToUserIdentifier());
await _appNotifier.WelcomeToTheApplicationAsync(user);
///DUPLICATED (I can see a reason this wouldnt be duplicated for some subclassed users)
//Organization Units
await UserManager.SetOrganizationUnitsAsync(user, input.OrganizationUnits.ToArray());
///DUPLICATED (I can see a reason this wouldnt be duplicated for some subclassed users)
//Send activation email
if (input.SendActivationEmail)
{
user.SetNewEmailConfirmationCode();
await _userEmailer.SendEmailActivationLinkAsync(
user,
AppUrlService.CreateEmailActivationUrlFormat(AbpSession.TenantId),
input.User.Password
);
}
}
That is just for one method in the UserAppService. So if there are updates to the framework, I'm worried that unless we go through the diffs we would miss those updates (because we aren't subclassing the service, it could go completely unnoticed)
Maybe this is unavoidable, but I'm just worried that there is a code-smell here where subclassing involves duplication a lot of code. Was AspNetZero/ABP built with subclassing User as a supported pattern? It seems like some of this behavior could be consolidated or handled by events. (That would make creating services for subclassed users a much smaller undertaking without worrying about losing other behaviours like notifications or welcome emails because they were overlooked)
I think the solution proposed will work for us, I just want to be absolutely certain before I develop our subclassed user app services such that they are detached from the original UserAppService as that will be a non-trivial time sink.