Many Thanks for you help. Our workaround is very similar. We had multiple issues with the DefaultRedisCacheSerializer:
This is our cache serializer :
using Abp.Dependency; using Abp.Runtime.Caching.Redis; using Abp.Runtime.Caching; using StackExchange.Redis; using System; using System.Text.Encodings.Web; using System.Text.Json; using Abp.Json; using System.Text.Json.Serialization; using Abp.Json.SystemTextJson;
namespace RmoniWeb.Helpers { public class RmoniWebRedisCacheSerializer : IRedisCacheSerializer, ITransientDependency { /// <summary> /// Creates an instance of the object from its serialized string representation. /// </summary> /// <param name="objbyte">String representation of the object from the Redis server.</param> /// <returns>Returns a newly constructed object.</returns> /// <seealso cref="IRedisCacheSerializer{TSource, TDestination}.Serialize" /> public virtual object Deserialize(RedisValue objbyte) private readonly JsonSerializerOptions _simpleJsonSerializerOptions = new() { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, ReferenceHandler = ReferenceHandler.Preserve };
private readonly JsonSerializerOptions _jsonSerializerOptionsWithConverters = new()
{
ReadCommentHandling = JsonCommentHandling.Skip,
AllowTrailingCommas = true,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
TypeInfoResolver = new AbpDateTimeJsonTypeInfoResolver(),
ReferenceHandler = ReferenceHandler.Preserve,
Converters =
{
new AbpStringToEnumFactory(),
new AbpStringToBooleanConverter(),
new AbpStringToGuidConverter(),
new AbpNullableStringToGuidConverter(),
new AbpNullableFromEmptyStringConverterFactory(),
new ObjectToInferredTypesConverter(),
new AbpJsonConverterForType(),
new ExternalLoginProviderInfoConverter(),
}
};
/// <summary>
/// Creates an instance of the object from its serialized string representation.
/// </summary>
/// <param name="objbyte">String representation of the object from the Redis server.</param>
/// <returns>Returns a newly constructed object.</returns>
/// <seealso cref="IRedisCacheSerializer{TSource, TDestination}.Serialize" />
public virtual object Deserialize(RedisValue objbyte)
{
if ((string)objbyte is null)
return null;
var convertedValue = JsonSerializer.Deserialize<AbpCacheData>(objbyte, _jsonSerializerOptionsWithConverters);
var type = Type.GetType(convertedValue.Type, true, true);
return convertedValue.Payload.FromJsonString(type, _jsonSerializerOptionsWithConverters);
}
private readonly JsonSerializerOptions _simpleJsonSerializerOptions = new()
{
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
ReferenceHandler = ReferenceHandler.Preserve
};
private readonly JsonSerializerOptions _jsonSerializerOptionsWithConverters = new()
{
ReadCommentHandling = JsonCommentHandling.Skip,
AllowTrailingCommas = true,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
TypeInfoResolver = new AbpDateTimeJsonTypeInfoResolver(),
ReferenceHandler = ReferenceHandler.Preserve,
Converters =
{
new AbpStringToEnumFactory(),
new AbpStringToBooleanConverter(),
new AbpStringToGuidConverter(),
new AbpNullableStringToGuidConverter(),
new AbpNullableFromEmptyStringConverterFactory(),
new ObjectToInferredTypesConverter(),
new AbpJsonConverterForType(),
new ExternalLoginProviderInfoConverter(),
}
};
/// <summary>
/// Produce a string representation of the supplied object.
/// </summary>
/// <param name="value">Instance to serialize.</param>
/// <param name="type">Type of the object.</param>
/// <returns>Returns a string representing the object instance that can be placed into the Redis cache.</returns>
/// <seealso cref="IRedisCacheSerializer{TSource, TDestination}.Deserialize" />
public virtual RedisValue Serialize(object value, Type type)
{
var typeString = TypeHelper.SerializeType(value.GetType()).ToString();
var convertedValue = JsonSerializer.Serialize(value, _jsonSerializerOptionsWithConverters);
var json = new AbpCacheData(typeString, convertedValue);
return JsonSerializer.Serialize(json, _jsonSerializerOptionsWithConverters);
}
}
}
This is the converter for ExterLoginProviderInfo: using Abp.AspNetZeroCore.Web.Authentication.External; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading.Tasks;
namespace RmoniWeb.Helpers { public class ExternalLoginProviderInfoConverter : JsonConverter<ExternalLoginProviderInfo> { private Dictionary<string, string> ReadDictionary(ref Utf8JsonReader reader) { Dictionary<string, string> dictionary = new Dictionary<string, string>(); while (reader.Read()) { if (reader.TokenType == JsonTokenType.EndObject) { break; } if (reader.TokenType != JsonTokenType.StartObject) { throw new JsonException("Expected StartObject token."); }
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndObject)
{
// End of the current JsonClaimMap object, break
break;
}
// Read the key (property name)
if (reader.TokenType == JsonTokenType.PropertyName)
{
string key = reader.GetString();
// Read the value
reader.Read();
if (reader.TokenType != JsonTokenType.Null && reader.TokenType != JsonTokenType.String)
{
throw new JsonException("Expected string value.");
}
string value = reader.GetString();
// Add the key-value pair to the dictionary
dictionary[key] = value;
}
}
}
return dictionary;
}
private List<JsonClaimMap> ReadJsonClaimMapList(ref Utf8JsonReader reader)
{
List<JsonClaimMap> claimMapList = new List<JsonClaimMap>();
// Ensure we are at the start of an array
//if (reader.TokenType != JsonTokenType.StartArray)
//{
// throw new JsonException("Expected StartArray token.");
//}
// Read the array
while (reader.Read())
{
// Break if we reach the end of the array
if (reader.TokenType == JsonTokenType.EndArray)
{
break;
}
// Ensure we're starting an object
if (reader.TokenType != JsonTokenType.StartArray)
{
throw new JsonException("Expected StartArray token.");
}
// Create a new JsonClaimMap instance
JsonClaimMap claimMap = new JsonClaimMap();
// Read properties within the object
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndObject)
{
// End of the current JsonClaimMap object, break
break;
}
// Get the property name
if (reader.TokenType == JsonTokenType.PropertyName)
{
string propertyName = reader.GetString();
// Read the next token (which should be the value)
if (!reader.Read())
{
throw new JsonException("Expected a value after property name.");
}
// Assign the property based on the property name
if (propertyName == "Claim")
{
claimMap.Claim = reader.GetString();
}
else if (propertyName == "Key")
{
claimMap.Key = reader.GetString();
}
}
}
// Add the parsed JsonClaimMap to the list
claimMapList.Add(claimMap);
}
return claimMapList;
}
public override ExternalLoginProviderInfo Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
// Create a temporary object to hold the JSON data
Dictionary<string, object> tempObject = JsonSerializer.Deserialize<Dictionary<string, object>>(ref reader, options);
// Extract the properties manually
string name = tempObject["Name"]?.ToString();
string clientId = tempObject["ClientId"]?.ToString();
string clientSecret = tempObject["ClientSecret"]?.ToString();
Type providerApiType = Type.GetType(tempObject["ProviderApiType"].ToString());
//Dictionary<string, string> additionalParams = JsonSerializer.Deserialize<Dictionary<string, string>>(tempObject["AdditionalParams"].ToString());
byte[] paramBytes = Encoding.UTF8.GetBytes(tempObject["AdditionalParams"].ToString());
Type providerApiType = Type.GetType(tempObject["ProviderApiType"]?.ToString());
byte[] paramBytes = Encoding.UTF8.GetBytes(tempObject["AdditionalParams"]?.ToString());
ReadOnlySpan<byte> paramSpan = new ReadOnlySpan<byte>(paramBytes);
var paramReader = new Utf8JsonReader(paramSpan);
var additionalParams = ReadDictionary(ref paramReader);
byte[] claimBytes = Encoding.UTF8.GetBytes(tempObject["ClaimMappings"].ToString());
byte[] claimBytes = Encoding.UTF8.GetBytes(tempObject["ClaimMappings"]?.ToString());
ReadOnlySpan<byte> claimSpan = new ReadOnlySpan<byte>(claimBytes);
var claimReader = new Utf8JsonReader(claimSpan);
var claimMappings = ReadJsonClaimMapList(ref claimReader);
// Create the ExternalLoginProviderInfo instance using the constructor
return new ExternalLoginProviderInfo(name, clientId, clientSecret, providerApiType, additionalParams, claimMappings);
}
public override void Write(Utf8JsonWriter writer, ExternalLoginProviderInfo value, JsonSerializerOptions options)
{
// Handle serialization if needed
writer.WriteStartObject();
writer.WriteString("Name", value.Name);
writer.WriteString("ClientId", value.ClientId);
writer.WriteString("ClientSecret", value.ClientSecret);
var assemblyQualifiedName = TypeHelper.SerializeType(value.ProviderApiType).ToString();
writer.WriteString("ProviderApiType", assemblyQualifiedName);
writer.WritePropertyName("AdditionalParams");
writer.WriteStartObject();
foreach (var kvp in value.AdditionalParams)
{
// Write each key-value pair as a JSON property
writer.WriteString(kvp.Key, kvp.Value);
}
writer.WriteEndObject();
writer.WritePropertyName("ClaimMappings");
writer.WriteStartArray();
foreach (var claim in value.ClaimMappings)
{
writer.WriteStartObject();
writer.WriteString("Claim", claim.Claim);
writer.WriteString("Key", claim.Key);
writer.WriteEndObject();
}
writer.WriteEndArray();
writer.WriteEndObject();
}
}
private static Dictionary<string, string> ReadDictionary(ref Utf8JsonReader reader)
{
var dictionary = new Dictionary<string, string>();
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndObject)
{
break;
}
if (reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException("Expected StartObject token.");
}
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndObject)
{
// End of the current JsonClaimMap object, break
break;
}
// Read the key (property name)
if (reader.TokenType == JsonTokenType.PropertyName)
{
var key = reader.GetString();
// Read the value
reader.Read();
if (reader.TokenType != JsonTokenType.Null && reader.TokenType != JsonTokenType.String)
{
throw new JsonException("Expected string value.");
}
var value = reader.GetString();
// Add the key-value pair to the dictionary
if (key != null)
dictionary[key] = value;
}
}
}
return dictionary;
}
private static List<JsonClaimMap> ReadJsonClaimMapList(ref Utf8JsonReader reader)
{
List<JsonClaimMap> claimMapList = new List<JsonClaimMap>();
// Read the array
while (reader.Read())
{
// Break if we reach the end of the array
if (reader.TokenType == JsonTokenType.EndArray)
{
break;
}
// Ensure we're starting an object
if (reader.TokenType != JsonTokenType.StartArray)
{
throw new JsonException("Expected StartArray token.");
}
// Create a new JsonClaimMap instance
JsonClaimMap claimMap = new JsonClaimMap();
// Read properties within the object
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndObject)
{
// End of the current JsonClaimMap object, break
break;
}
// Get the property name
if (reader.TokenType == JsonTokenType.PropertyName)
{
string propertyName = reader.GetString();
// Read the next token (which should be the value)
if (!reader.Read())
{
throw new JsonException("Expected a value after property name.");
}
// Assign the property based on the property name
if (propertyName == "Claim")
{
claimMap.Claim = reader.GetString();
}
else if (propertyName == "Key")
{
claimMap.Key = reader.GetString();
}
}
}
// Add the parsed JsonClaimMap to the list
claimMapList.Add(claimMap);
}
return claimMapList;
}
}
}
Hi,
If I click on the first link, I get a 404. Is this link correct?
From which version on the switch was done from Newtonsoft.Json to System.Text.Json? If I look at the release notes this was in 9.1. We are on 9.2.2.
We are using ASP.net Zero (Angular + ASP.Net) already for sometime and have build a custom application based on it. Until now our application is running on one server. Recently we have build a clustered environment where ASP.net is running on 2 servers behind a load-balancer. Both are running on the same database.
The issue happens when we first login as a host-admin. Then we select a tenant and login as that tenant. And then we logout again. The issue happens when the login-page (with tenant logo) opens again. Meanwhile we have built a workaround where we have developed a JsonConverter for ExternalLoginProviderInfo
We are using ASP.net Zero based on ABP 9.2.2. When opening a login page (where a tenant is already selected), an exception is thrown in the backend :
System.NotSupportedException: 'Reference metadata is not supported when deserializing constructor parameters. See type 'Abp.AspNetZeroCore.Web.Authentication.External.ExternalLoginProviderInfo'. The unsupported member type is located on type 'System.Collections.Generic.Dictionary`2[System.String,System.String]'. Path: $.AdditionalParams.$ref | LineNumber: 0 | BytePositionInLine: 284.'
How can this be solved?
I am sorry, I forgot I had to upgrade also other ABP packages.
We had an issue with Azure SignalR and Redis Cache in a clustered environment. I saw that we needed to upgrade to ABP 9.1.3. After upgrading and deploying our ASP.Net Zero based application, we got an error message in the Event Viewer of Windows :
Application: w3wp.exe
CoreCLR Version: 8.0.324.11423
.NET Version: 8.0.3
Description: The process was terminated due to an unhandled exception.
Exception Info: System.TypeLoadException: Could not load type 'Abp.RealTime.IOnlineClientStore1' from assembly 'Abp, Version=9.1.3.0, Culture=neutral, PublicKeyToken=null'. at Abp.Runtime.Caching.Redis.RedisCacheConfigurationExtensions.UseRedis(ICachingConfiguration cachingConfiguration, Action
1 optionsAction)
at RmoniWeb.Web.RmoniWebWebCoreModule.PreInitialize() in D:\a\1\s\src\RmoniWeb.Web.Core\RmoniWebWebCoreModule.cs:line 64
at Abp.Modules.AbpModuleManager.<>c.<StartModules>b__15_0(AbpModuleInfo module)
at System.Collections.Generic.List1.ForEach(Action
1 action)
at Abp.Modules.AbpModuleManager.StartModules()
at Abp.AbpBootstrapper.Initialize()
at Abp.AspNetCore.AbpApplicationBuilderExtensions.InitializeAbp(IApplicationBuilder app)
at Abp.AspNetCore.AbpApplicationBuilderExtensions.UseAbp(IApplicationBuilder app, Action1 optionsAction) at RmoniWeb.Web.Startup.Startup.Configure(IApplicationBuilder app, IWebHostEnvironment env, RmoniWebDbContext dbContext) in D:\a\1\s\src\RmoniWeb.Web.Host\Startup\Startup.cs:line 220 at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor) at System.Reflection.MethodBaseInvoker.InvokeDirectByRefWithFewArgs(Object obj, Span
1 copyOfArgs, BindingFlags invokeAttr)
at System.Reflection.MethodBaseInvoker.InvokeWithFewArgs(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
at Microsoft.AspNetCore.Hosting.ConfigureBuilder.Invoke(Object instance, IApplicationBuilder builder)
at Microsoft.AspNetCore.Hosting.ConventionBasedStartup.Configure(IApplicationBuilder app)
at Microsoft.ApplicationInsights.AspNetCore.ApplicationInsightsStartupFilter.<>c__DisplayClass2_0.<Configure>b__0(IApplicationBuilder app)
at Microsoft.Azure.SignalR.AzureSignalRStartupFilter.<>c__DisplayClass0_0.<Configure>b__0(IApplicationBuilder app)
at Microsoft.AspNetCore.Hosting.WebHost.BuildApplication()
at Microsoft.AspNetCore.Hosting.WebHost.StartAsync(CancellationToken cancellationToken)
at Microsoft.AspNetCore.Hosting.WebHostExtensions.RunAsync(IWebHost host, CancellationToken token, String startupMessage)
at Microsoft.AspNetCore.Hosting.WebHostExtensions.RunAsync(IWebHost host, CancellationToken token, String startupMessage)
at Microsoft.AspNetCore.Hosting.WebHostExtensions.RunAsync(IWebHost host, CancellationToken token)
at Microsoft.AspNetCore.Hosting.WebHostExtensions.Run(IWebHost host)
at RmoniWeb.Web.Startup.Program.Main(String[] args) in D:\a\1\s\src\RmoniWeb.Web.Host\Startup\Program.cs:line 12
The project should be in your inbox, Please let us know if you require additional information
The AuditExpansion attribute is used to provide extra context for certain properties in the UI.
On Application start-up, a dictionary is built based on these attributes, so the endpoint that fetches the data knows where to fetch context.
So if an entity has a UserId with a value of 5 that would be displayed as 5 (UserName)
That being said, The issue is happening without the attribute as well, and the duplicate properties are inserted in the DB on save, before we even attempt to fetch it for the front-end. at this point, no action has been taken yet due to the attribute
[AbpAuthorize(AppPermissions.Pages_Administration_Users_Edit)]
protected virtual async Task UpdateUserAsync(CreateOrUpdateUserInput input)
{
var user = await UserManager.FindByIdAsync(input.User.Id.Value.ToString());
//Update user properties
ObjectMapper.Map(input.User, user); //Passwords is not mapped (see mapping configuration)
CheckErrors(await UserManager.UpdateAsync(user));
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);
CheckErrors(await UserManager.ChangePasswordAsync(user, input.User.Password));
}
//Update roles
CheckErrors(await UserManager.SetRolesAsync(user, input.AssignedRoleNames));
//update organization units
await UserManager.SetOrganizationUnitsAsync(user, input.OrganizationUnits.ToArray());
if (AbpSession.TenantId.HasValue)
{
await _cacheManager.GetUserOrganizationUnitIdsRecursiveCache(AbpSession.GetTenantId()).ClearAsync();
await _cacheManager.GetUserOrganizationUnitIdsCache(AbpSession.GetTenantId()).ClearAsync();
await _cacheManager.GetUserOrganizationUnitsRecursiveCache(AbpSession.GetTenantId()).ClearAsync();
}
if (input.SendActivationEmail)
{
user.SetNewEmailConfirmationCode();
await _userEmailer.SendEmailActivationLinkAsync(
user,
AppUrlService.CreateEmailActivationUrlFormat(AbpSession.TenantId),
input.User.Password
);
}
}
UserManager custom implementations:
public override Task<IdentityResult> SetRolesAsync(User user, string[] roleNames)
{
if (user.Name == "admin" && !roleNames.Contains(StaticRoleNames.Host.Admin))
{
throw new UserFriendlyException(L("AdminRoleCannotRemoveFromAdminUser"));
}
return base.SetRolesAsync(user, roleNames);
}