Remove the default after adding a required column if HasDefaultValue wasn't used
- Dominant language
- C#
- Stars
- 14.8k
- Forks
- 3.4k
- PR merge metrics
- PR metrics pending
Description
### Bug description
I have a `User` entity which has a `UserAccessStatus` property configured as an owned entity. This, in turn, as a boolean property named `HasEmailAccess`, which is configured as follows in the migration configuration class:
```csharp
builder.Property(accessStatus => accessStatus.HasEmailAccess).IsRequired().HasDefaultValue(false);
```
Although the above configuration seems redundant (after all, the default `bool` value is `false`) it was required to make sure that my migration file (for a PostgreSQL database) was created correctly, setting the column as required and with default value as false.
However, with this configuration when I try to update the owned entity `HasEmailAccess` property value (effectively deleting and recreating another instance), it works only for one side: when I update its value from `false` to `true`. The other way around does not work.
For now, I'm removing the `HasDefaultValue` clause from the configuration (which, IMO, should not be necessary anyway), but I'd highly appreciate any inputs you guys could provide on this matter. Please let me know if any further info is required.
### Your code
```csharp
internal sealed class UserConfiguration : IEntityTypeConfiguration
{
public void Configure(EntityTypeBuilder builder)
{
builder.ToTable(TableNames.Users);
builder.Property(user => user.Id)
.ValueGeneratedNever()
.HasGuidConversion(UserId.FromGuid);
builder.OwnsOne(user => user.AccessStatus, ConfigureUserAccessStatus);
builder.OwnsOne(user => user.Profile, ConfigureUserProfile);
builder.OwnsOne(user => user.Preferences, ConfigureUserPreferences);
builder.HasMany(user => user.Permissions)
.WithOne()
.HasForeignKey(userPermission => userPermission.UserId);
builder.HasMany(user => user.Roles)
.WithOne()
.HasForeignKey(userRole => userRole.UserId);
builder.HasIndex(user => user.Email).IsUnique();
builder.HasIndex(user => user.IdentityProviderId).IsUnique();
}
#region Owned entities configuration methods
private static void ConfigureUserAccessStatus(OwnedNavigationBuilder builder)
{
builder.ToTable(TableNames.UsersAccessStatusDetails);
builder.Property(accessStatus => accessStatus.Type).IsRequired();
builder.Property(accessStatus => accessStatus.HasEmailAccess).IsRequired(); // .HasDefaultValue(false); --> More details in https://github.com/npgsql/efcore.pg/issues/3470
builder.Property(accessStatus => accessStatus.AccessGranterUserName).IsRequired(false);
builder.Property(accessStatus => accessStatus.AccessGrantedOn).IsRequired(false);
builder.Property(accessStatus => accessStatus.AccessRevokerUserName).IsRequired(false);
builder.Property(accessStatus => accessStatus.AccessRevokedOn).IsRequired(false);
}
private static void ConfigureUserProfile(OwnedNavigationBuilder builder)
{
builder.ToTable(TableNames.UsersProfileDetails);
builder.Property(profile => profile.PersonId)
.HasNullableGuidConversion(PersonId.FromGuid);
builder.Property(profile => profile.FullName).IsRequired();
builder.Property(profile => profile.CpfCnpj).IsRequired();
builder.Property(profile => profile.Phone).HasMaxLength(MaxLengths.PhoneNumber);
builder.Property(profile => profile.ProfilePicture).IsRequired(false);
builder.HasIndex(profile => profile.PersonId).IsUnique();
}
private static void ConfigureUserPreferences(OwnedNavigationBuilder builder)
{
builder.ToTable(TableNames.UsersPreferencesDetails);
builder.Property(preferences => preferences.IsDarkMode).IsRequired(false);
builder.Property(preferences => preferences.EmailNotificationsEnabled).IsRequired();
builder.Property(preferences => preferences.SystemNotificationsEnabled).IsRequired();
}
#endregion
}
public sealed class User : Entity, IAuditable
{
private readonly HashSet _permissions = [];
private readonly HashSet _roles = [];
private User(UserId id, Guid identityProviderId, bool hasEmailAccess, string fullName, CpfCnpj cpfCnpj, EmailAddress email,
string phone, string? profilePicture)
: base(id)
{
IdentityProviderId = identityProviderId;
Email = email;
Profile = new UserProfile(fullName, cpfCnpj, phone, null, profilePicture);
Preferences = new UserPreferences();
AccessStatus = new UserAccessStatus(UserAccessStatusType.Waiting, hasEmailAccess);
CreatedOn = TimeProvider.System.GetUtcNow();
}
private User() { } // Necessary for EF Core Configuration
///
/// Id provided by the identity provider module.
///
public Guid IdentityProviderId { get; }
///
/// User's e-mail (which is theirs username).
///
public EmailAddress Email { get; private set; }
///
/// User's profile data.
///
public UserProfile Profile { get; private set; } = default!;
///
/// User's system preferences.
///
public UserPreferences Preferences { get; private set; } = default!;
///
/// Last date/time user signed-in to the system.
///
public DateTimeOffset? LastSignInOn { get; private set; }
///
/// Contains data related to the status of the user's access to the system.
///
public UserAccessStatus AccessStatus { get; private set; } = default!;
public DateTimeOffset CreatedOn { get; private init; }
public DateTimeOffset? UpdatedOn { get; private set; }
public IReadOnlyCollection Permissions => _permissions.ToList().AsReadOnly();
public IReadOnlyCollection Roles => _roles.ToList().AsReadOnly();
///
/// Creates a new user with the specified parameters.
///
/// The id provided by the identity provider module.
/// True if user has an e-mail/password access to the system or
/// false if only external provider(s) is(are) used.
/// User's full name.
/// User's CPF/CNPJ.
/// User's e-mail (its username).
/// User's phone number.
/// User's .
/// Id of the condominium for which the applies.
/// External login provider name (if any).
/// Profile picture URL (if any).
/// The new user instance.
public static Result Create(
Guid identityProviderId,
bool hasEmailAccess,
string fullName,
CpfCnpj cpfCnpj,
EmailAddress email,
string phone,
Role role,
CondominiumId roleCondominiumId,
string? loginProvider,
string? profilePictureUrl)
{
var user = new User(UserId.New(), identityProviderId, hasEmailAccess, fullName, cpfCnpj, email, phone, profilePictureUrl);
user._roles.Add(new UserRole(user.Id, role, roleCondominiumId));
user.RaiseDomainEvent(new UserCreatedDomainEvent(Guid.NewGuid(), GetEventOccurredDateTime(), user.Id,
identityProviderId, fullName, email, loginProvider, user.Preferences.EmailNotificationsEnabled,
user.Preferences.SystemNotificationsEnabled));
return user;
}
///
/// Changes the user's e-mail (username).
///
/// The new e-mail.
public Result ChangeEmail(EmailAddress newEmail)
{
Email = newEmail;
RaiseDomainEvent(new UserEmailChangedDomainEvent(Guid.NewGuid(), GetEventOccurredDateTime(),
IdentityProviderId, Profile.FullName, newEmail));
return Result.Success();
}
///
/// Changes the flag that indicates if user may access the system using e-mail/password.
///
/// True if user has added e-mail/password access
/// or false if this option has been removed.
public Result ChangeEmailAccess(bool hasEmailAccess)
{
AccessStatus = AccessStatus.ChangeEmailAccess(hasEmailAccess);
return Result.Success();
}
///
/// Sets the user's profile person id.
///
/// Person id to set.
public Result SetProfilePersonId(PersonId personId)
{
Profile = Profile.WithPersonId(personId);
return Result.Success();
}
///
/// Sets the user's profile picture as a URI.
///
/// URI of profile picture.
public Result SetProfilePictureUri(string profilePictureUri)
{
Profile = Profile.WithProfilePictureUri(profilePictureUri);
return Result.Success();
}
///
/// Sets the user system access as granted.
///
/// Granter user's username.
/// Date/time when access has been granted.
public Result SetAccessStatusAsGranted(EmailAddress? granterUserName, DateTimeOffset? grantedOn)
{
AccessStatus = AccessStatus.SetAccessGranted(granterUserName, grantedOn);
return Result.Success();
}
///
/// Sets the user system access as revoked.
///
/// Revoker user's username.
/// Date/time when access has been revoked.
public Result SetAccessStatusAsRevoked(EmailAddress? revokerUserName, DateTimeOffset? revokedOn)
{
AccessStatus = AccessStatus.SetAccessRevoked(revokerUserName, revokedOn);
return Result.Success();
}
///
/// Update user's permissions.
///
/// Permissions to add to user.
/// Permissions to remove from user.
/// User with updated permissions list.
public Result UpdatePermissions(
List<(string PermissionName, CondominiumId CondominiumId)> permissionsToAdd,
List<(string PermissionName, CondominiumId CondominiumId)> permissionsToRemove)
{
foreach (var permissionTuple in permissionsToAdd)
{
var permission = Permission.FromName(permissionTuple.PermissionName)!;
_permissions.Add(new UserPermission(Id, permission, permissionTuple.CondominiumId));
}
foreach (var permissionTuple in permissionsToRemove)
{
var permission = Permission.FromName(permissionTuple.PermissionName)!;
_permissions.RemoveWhere(x => x.Permission == permission && x.CondominiumId == permissionTuple.CondominiumId);
}
return this;
}
///
/// Updates user's profile data.
///
/// Person's full name.
/// Person's CPF or CNPJ.
/// Person's main phone number.
/// Person's profile picture.
/// User instance with updated data.
public Result UpdateProfileData(string fullName, CpfCnpj cpfCnpj, string phone, string? profilePicture)
{
Profile = Profile.With(fullName, cpfCnpj, phone, profilePicture);
return this;
}
///
/// Update user's preferences.
///
/// Flag which indicates if user uses light or dark mode.
/// Flag which indicates if notifications by e-mail are enabled.
/// Flag which indicates if system notifications are enabled.
public Result UpdatePreferences(bool isDarkMode, bool emailNotificationsEnabled, bool systemNotificationsEnabled)
{
Preferences = Preferences.With(isDarkMode, emailNotificationsEnabled, systemNotificationsEnabled);
RaiseDomainEvent(new UserPreferencesUpdatedDomainEvent(Guid.NewGuid(), GetEventOccurredDateTime(), Id,
Preferences.EmailNotificationsEnabled, Preferences.SystemNotificationsEnabled));
return Result.Success();
}
}
public sealed class UserAccessStatus : ValueObject
{
public UserAccessStatus(UserAccessStatusType type, bool hasEmailAccess,
EmailAddress? accessGranterUserName = null, DateTimeOffset? accessGrantedOn = null,
EmailAddress? accessRevokerUserName = null, DateTimeOffset? accessRevokedOn = null)
{
Type = type;
HasEmailAccess = hasEmailAccess;
AccessGranterUserName = accessGranterUserName;
AccessGrantedOn = accessGrantedOn;
AccessRevokerUserName = accessRevokerUserName;
AccessRevokedOn = accessRevokedOn;
}
private UserAccessStatus() { } // Necessary for EF Core Configuration
public UserAccessStatusType Type { get; } = default!;
public bool HasEmailAccess { get; }
public EmailAddress? AccessGranterUserName { get; }
public DateTimeOffset? AccessGrantedOn { get; }
public EmailAddress? AccessRevokerUserName { get; }
public DateTimeOffset? AccessRevokedOn { get; }
protected override IEnumerable GetAtomicValues()
{
yield return Type;
yield return HasEmailAccess;
yield return AccessGranterUserName ?? default!;
yield return AccessGrantedOn ?? DateTimeOffset.MinValue;
yield return AccessRevokerUserName ?? default!;
yield return AccessRevokedOn ?? DateTimeOffset.MinValue;
}
///
/// Changes the flag that indicates if user may access the system using e-mail/password.
///
/// True if user has added e-mail/password access
/// or false if this option has been removed.
/// New instance of status with flag value changed.
public UserAccessStatus ChangeEmailAccess(bool hasEmailAccess) =>
new(Type, hasEmailAccess, AccessGranterUserName, AccessGrantedOn, AccessRevokerUserName, AccessRevokedOn);
///
/// Sets the user access status as granted.
///
/// Granter user's username.
/// Date/time when access has been granted.
/// New instance of status with status as and granter data set.
public UserAccessStatus SetAccessGranted(EmailAddress? granterUserName, DateTimeOffset? grantedOn) =>
new(UserAccessStatusType.Active, HasEmailAccess, granterUserName, grantedOn);
///
/// Sets the user access status as revoked.
///
/// Revoker user's username.
/// Date/time when access has been revoked.
/// New instance of status with status as and revoker data set.
public UserAccessStatus SetAccessRevoked(EmailAddress? revokerUserName, DateTimeOffset? revokedOn) =>
new(UserAccessStatusType.Revoked, HasEmailAccess, AccessGranterUserName, AccessGrantedOn, revokerUserName, revokedOn);
}
```
### Stack traces
```text
```
### Verbose output
```text
```
### EF Core version
8.0.11
### Database provider
Npgsql.EntityFrameworkCore.PostgreSQL
### Target framework
.NET 8.0
### Operating system
Windows 11
### IDE
Visual Studio 2022 17.13.2
Contributor guide
Assessment
This issue has not been assessed yet.