Base solution for your next web application

Activities of "dirkvr"

Many Thanks for you help. Our workaround is very similar. We had multiple issues with the DefaultRedisCacheSerializer:

  • We wanted to serialize an array with circular references
  • We had issues with ApiProviderType : which is also solved with your workaround
  • We had issues with serializing the property "AdditionalParams" of ExternalLoginProviderInfo. This is solved with a separate converter for ExternalLogproviderInfo.

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(),
            }
    };

    /// &lt;summary&gt;
    ///     Creates an instance of the object from its serialized string representation.
    /// &lt;/summary&gt;
    /// &lt;param name=&quot;objbyte&quot;&gt;String representation of the object from the Redis server.&lt;/param&gt;
    /// &lt;returns&gt;Returns a newly constructed object.&lt;/returns&gt;
    /// &lt;seealso cref=&quot;IRedisCacheSerializer{TSource, TDestination}.Serialize&quot; /&gt;
    public virtual object Deserialize(RedisValue objbyte)
	{
		if ((string)objbyte is null)
			return null;

		var convertedValue = JsonSerializer.Deserialize&lt;AbpCacheData&gt;(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(),
			}
	};

	/// &lt;summary&gt;
	///     Produce a string representation of the supplied object.
	/// &lt;/summary&gt;
	/// &lt;param name=&quot;value&quot;&gt;Instance to serialize.&lt;/param&gt;
	/// &lt;param name=&quot;type&quot;&gt;Type of the object.&lt;/param&gt;
	/// &lt;returns&gt;Returns a string representing the object instance that can be placed into the Redis cache.&lt;/returns&gt;
	/// &lt;seealso cref=&quot;IRedisCacheSerializer{TSource, TDestination}.Deserialize&quot; /&gt;
	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&lt;JsonClaimMap&gt; ReadJsonClaimMapList(ref Utf8JsonReader reader)
	{
		List&lt;JsonClaimMap&gt; claimMapList = new List&lt;JsonClaimMap&gt;();

		// 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&lt;string, object&gt; tempObject = JsonSerializer.Deserialize&lt;Dictionary&lt;string, object&gt;>(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&lt;string, string&gt; additionalParams = JsonSerializer.Deserialize&lt;Dictionary&lt;string, string&gt;>(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&lt;byte&gt; paramSpan = new ReadOnlySpan&lt;byte&gt;(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&lt;byte&gt; claimSpan = new ReadOnlySpan&lt;byte&gt;(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&lt;string, string&gt; ReadDictionary(ref Utf8JsonReader reader)
    {
        var dictionary = new Dictionary&lt;string, string&gt;();
        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&lt;JsonClaimMap&gt; ReadJsonClaimMapList(ref Utf8JsonReader reader)
    {
        List&lt;JsonClaimMap&gt; claimMapList = new List&lt;JsonClaimMap&gt;();

        // 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, Action1 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(Action1 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, Span1 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);
}
Showing 1 to 10 of 13 entries