Skip to content

Commit

Permalink
STJ: support custom converters for view models
Browse files Browse the repository at this point in the history
  • Loading branch information
exyi committed Apr 26, 2024
1 parent dcba751 commit 61796d5
Show file tree
Hide file tree
Showing 14 changed files with 237 additions and 190 deletions.
70 changes: 0 additions & 70 deletions src/DotVVM.Stryker.sln

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ public static IServiceCollection RegisterDotVVMServices(IServiceCollection servi
services.TryAddSingleton<IValidationErrorPathExpander, ValidationErrorPathExpander>();
services.TryAddSingleton<IViewModelValidator, ViewModelValidator>();
services.TryAddSingleton<IStaticCommandArgumentValidator, StaticCommandArgumentValidator>();
services.TryAddSingleton<IDotvvmJsonOptionsProvider, DotvvmJsonOptionsProvider>();
services.TryAddSingleton<IViewModelSerializationMapper, ViewModelSerializationMapper>();
services.TryAddSingleton<ViewModelJsonConverter>();
services.TryAddSingleton<IViewModelParameterBinder, AttributeViewModelParameterBinder>();
Expand Down
4 changes: 2 additions & 2 deletions src/Framework/Framework/Hosting/StaticCommandExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,15 @@ public class StaticCommandExecutor
private readonly DotvvmConfiguration configuration;
private readonly JsonSerializerOptions jsonOptions;

public StaticCommandExecutor(IStaticCommandServiceLoader serviceLoader, IViewModelSerializer serializer, IViewModelProtector viewModelProtector, IStaticCommandArgumentValidator validator, DotvvmConfiguration configuration)
public StaticCommandExecutor(IStaticCommandServiceLoader serviceLoader, IDotvvmJsonOptionsProvider jsonOptions, IViewModelProtector viewModelProtector, IStaticCommandArgumentValidator validator, DotvvmConfiguration configuration)
{
this.serviceLoader = serviceLoader;
this.viewModelProtector = viewModelProtector;
this.validator = validator;
this.configuration = configuration;
if (configuration.ExperimentalFeatures.UseDotvvmSerializationForStaticCommandArguments.Enabled)
{
this.jsonOptions = serializer.ViewModelJsonOptions;
this.jsonOptions = jsonOptions.ViewModelJsonOptions;
}
else
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@ public class DotvvmCustomPrimitiveTypeConverter : JsonConverterFactory

class InnerConverter<T>: JsonConverter<T> where T: IDotvvmPrimitiveType
{
private CustomPrimitiveTypeRegistration registration = ReflectionUtils.TryGetCustomPrimitiveTypeRegistration(typeof(T))!;
// TODO: make this converter factory?
private CustomPrimitiveTypeRegistration registration = ReflectionUtils.TryGetCustomPrimitiveTypeRegistration(typeof(T)) ?? throw new InvalidOperationException($"The type {typeof(T)} is not a custom primitive type!");
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType is JsonTokenType.String
Expand All @@ -30,7 +29,6 @@ or JsonTokenType.False
or JsonTokenType.Number)
{
// TODO: utf8 parsing?
var registration = ReflectionUtils.TryGetCustomPrimitiveTypeRegistration(typeToConvert)!;
var str = reader.TokenType is JsonTokenType.String ? reader.GetString() :
reader.HasValueSequence ? StringUtils.Utf8Decode(reader.ValueSequence.ToArray()) :
StringUtils.Utf8Decode(reader.ValueSpan);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,33 +40,22 @@ public record SerializationException(bool Serialize, Type? ViewModelType, string
private readonly IViewModelSerializationMapper viewModelMapper;
private readonly IViewModelServerCache viewModelServerCache;
private readonly IViewModelTypeMetadataSerializer viewModelTypeMetadataSerializer;
private readonly ViewModelJsonConverter viewModelConverter;
private readonly IDotvvmJsonOptionsProvider jsonOptions;
private readonly ILogger<DefaultViewModelSerializer>? logger;
public bool SendDiff { get; set; } = true;

public JsonSerializerOptions ViewModelJsonOptions { get; }
/// <summary> JsonOptions without the <see cref="ViewModelJsonConverter" /> </summary>
public JsonSerializerOptions PlainJsonOptions { get; }


/// <summary>
/// Initializes a new instance of the <see cref="DefaultViewModelSerializer"/> class.
/// </summary>
public DefaultViewModelSerializer(DotvvmConfiguration configuration, IViewModelProtector protector, IViewModelSerializationMapper serializationMapper, IViewModelServerCache viewModelServerCache, IViewModelTypeMetadataSerializer viewModelTypeMetadataSerializer, ViewModelJsonConverter viewModelConverter, ILogger<DefaultViewModelSerializer>? logger)
public DefaultViewModelSerializer(DotvvmConfiguration configuration, IViewModelProtector protector, IViewModelSerializationMapper serializationMapper, IViewModelServerCache viewModelServerCache, IViewModelTypeMetadataSerializer viewModelTypeMetadataSerializer, IDotvvmJsonOptionsProvider jsonOptions, ILogger<DefaultViewModelSerializer>? logger)
{
this.viewModelProtector = protector;
this.viewModelMapper = serializationMapper;
this.viewModelServerCache = viewModelServerCache;
this.viewModelTypeMetadataSerializer = viewModelTypeMetadataSerializer;
this.viewModelConverter = viewModelConverter;
this.jsonOptions = jsonOptions;
this.logger = logger;
this.ViewModelJsonOptions = new JsonSerializerOptions(DefaultSerializerSettingsProvider.Instance.SettingsHtmlUnsafe) {
Converters = { viewModelConverter },
WriteIndented = configuration.Debug,
};
this.PlainJsonOptions = new JsonSerializerOptions(DefaultSerializerSettingsProvider.Instance.SettingsHtmlUnsafe) {
WriteIndented = configuration.Debug,
};
}

/// <summary>
Expand Down Expand Up @@ -119,13 +108,13 @@ public string SerializeViewModel(IDotvvmRequestContext context, object? commandR

(int vmStart, int vmEnd) WriteViewModelJson(Utf8JsonWriter writer, IDotvvmRequestContext context, DotvvmSerializationState state)
{
var converter = this.viewModelConverter.GetConverterCached(context.ViewModel!.GetType());
var converter = jsonOptions.GetRootViewModelConverter(context.ViewModel!.GetType());

writer.WriteStartObject();
writer.Flush();
var vmStart = (int)writer.BytesCommitted; // needed for server side VM cache - we only store the object body, without $csrfToken and $encryptedValues

converter.WriteUntyped(writer, context.ViewModel, ViewModelJsonOptions, state, wrapObject: false);
converter.WriteUntyped(writer, context.ViewModel, jsonOptions.ViewModelJsonOptions, state, wrapObject: false);

writer.Flush();
var vmEnd = (int)writer.BytesCommitted;
Expand Down Expand Up @@ -163,8 +152,8 @@ public MemoryStream BuildViewModel(IDotvvmRequestContext context, object? comman

var buffer = new MemoryStream();
using (var writer = new Utf8JsonWriter(buffer, new JsonWriterOptions {
Indented = this.ViewModelJsonOptions.WriteIndented,
Encoder = ViewModelJsonOptions.Encoder,
Indented = jsonOptions.ViewModelJsonOptions.WriteIndented,
Encoder = jsonOptions.ViewModelJsonOptions.Encoder,
//SkipValidation = true, // for the hack with WriteRawValue
}))
{
Expand Down Expand Up @@ -293,7 +282,7 @@ public ReadOnlyMemory<byte> BuildStaticCommandResponse(IDotvvmRequestContext con

using var state = DotvvmSerializationState.Create(isPostback: true, context.Services);
var outputBuffer = new MemoryStream();
using (var writer = new Utf8JsonWriter(outputBuffer, new JsonWriterOptions { Indented = this.ViewModelJsonOptions.WriteIndented, Encoder = ViewModelJsonOptions.Encoder }))
using (var writer = new Utf8JsonWriter(outputBuffer, new JsonWriterOptions { Indented = jsonOptions.ViewModelJsonOptions.WriteIndented, Encoder = jsonOptions.ViewModelJsonOptions.Encoder }))
{
writer.WriteStartObject();
writer.WritePropertyName("result"u8);
Expand Down Expand Up @@ -329,7 +318,7 @@ private void WriteCommandData(object? data, Utf8JsonWriter writer, MemoryStream
Debug.Assert(DotvvmSerializationState.Current is {});
try
{
JsonSerializer.Serialize(writer, data, this.ViewModelJsonOptions);
JsonSerializer.Serialize(writer, data, jsonOptions.ViewModelJsonOptions);
}
catch (Exception ex)
{
Expand Down Expand Up @@ -395,7 +384,7 @@ public byte[] SerializeModelState(IDotvvmRequestContext context)
{
modelState = context.ModelState.Errors,
action = "validationErrors"
}, this.PlainJsonOptions);
}, jsonOptions.PlainJsonOptions);
}


Expand Down Expand Up @@ -472,8 +461,8 @@ public void PopulateViewModel(IDotvvmRequestContext context, ReadOnlyMemory<byte

Debug.Assert(context.ViewModel is not null);

var converter = this.viewModelConverter.GetConverterCached(context.ViewModel.GetType());
var newVM = converter.PopulateUntyped(ref reader, context.ViewModel.GetType(), context.ViewModel, ViewModelJsonOptions, state);
var converter = jsonOptions.GetRootViewModelConverter(context.ViewModel.GetType());
var newVM = converter.PopulateUntyped(ref reader, context.ViewModel.GetType(), context.ViewModel, jsonOptions.ViewModelJsonOptions, state);
// var helperObject = new DeserializationHelper() { ViewModel = context.ViewModel };
// var newHelper = converter.Populate(ref reader, JsonOptions, helperObject, state);
// Debug.Assert(newHelper == (object)helperObject);
Expand Down Expand Up @@ -507,7 +496,7 @@ public ActionInfo ResolveCommand(IDotvvmRequestContext context, DotvvmView view)
var args = data.TryGetProperty("commandArgs"u8, out var argsJson) ?
argsJson.EnumerateArray().Select(a => (Func<Type, object?>)(t => {
using var state = DotvvmSerializationState.Create(isPostback: true, context.Services, readEncryptedValues: new JsonObject());
return JsonSerializer.Deserialize(a, t, ViewModelJsonOptions);
return JsonSerializer.Deserialize(a, t, jsonOptions.ViewModelJsonOptions);
})).ToArray() :
new Func<Type, object?>[0];

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using System;
using System.Text.Json;
using DotVVM.Framework.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace DotVVM.Framework.ViewModel.Serialization;

/// <summary> Creates and provides System.Text.Json serialization options for ViewModel serialization </summary>
public interface IDotvvmJsonOptionsProvider
{
/// <summary> Options used for view model serialization, includes the <see cref="ViewModelJsonConverter" /> </summary>
JsonSerializerOptions ViewModelJsonOptions { get; }
/// <summary> Options used for serialization of other objects like the ModelState in the invalid VM response. </summary>
JsonSerializerOptions PlainJsonOptions { get; }

/// <summary> The the main converter used for viewmodel serialization and deserialization (in initial requests and commands) </summary>
IDotvvmJsonConverter GetRootViewModelConverter(Type type);
}


public class DotvvmJsonOptionsProvider : IDotvvmJsonOptionsProvider
{
private Lazy<JsonSerializerOptions> _viewModelOptions;
public JsonSerializerOptions ViewModelJsonOptions => _viewModelOptions.Value;
private Lazy<JsonSerializerOptions> _plainJsonOptions;
public JsonSerializerOptions PlainJsonOptions => _plainJsonOptions.Value;

private Lazy<ViewModelJsonConverter> _viewModelConverter;

public DotvvmJsonOptionsProvider(DotvvmConfiguration configuration)
{
var debug = configuration.Debug;
_viewModelConverter = new Lazy<ViewModelJsonConverter>(() => configuration.ServiceProvider.GetRequiredService<ViewModelJsonConverter>());
_viewModelOptions = new Lazy<JsonSerializerOptions>(() =>
new JsonSerializerOptions(DefaultSerializerSettingsProvider.Instance.SettingsHtmlUnsafe) {
Converters = { _viewModelConverter.Value },
WriteIndented = debug
}
);
_plainJsonOptions = new Lazy<JsonSerializerOptions>(() =>
!debug ? DefaultSerializerSettingsProvider.Instance.SettingsHtmlUnsafe
: new JsonSerializerOptions(DefaultSerializerSettingsProvider.Instance.SettingsHtmlUnsafe) { WriteIndented = true }
);
}

public IDotvvmJsonConverter GetRootViewModelConverter(Type type) => _viewModelConverter.Value.GetDotvvmConverter(type);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace DotVVM.Framework.ViewModel.Serialization;

/// <summary> System.Text.Json converter which supports population of existing objects. Implementations of this interface are also expected to implement <see cref="IDotvvmJsonConverter{T}" /> and inherit from <see cref="JsonConverter{T}" /> </summary>
public interface IDotvvmJsonConverter
{
public object? ReadUntyped(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, DotvvmSerializationState state);
public object? PopulateUntyped(ref Utf8JsonReader reader, Type typeToConvert, object? value, JsonSerializerOptions options, DotvvmSerializationState state);
public void WriteUntyped(Utf8JsonWriter writer, object? value, JsonSerializerOptions options, DotvvmSerializationState state, bool requireTypeField = true, bool wrapObject = true);
}

/// <summary> System.Text.Json converter which supports population of existing objects. </summary>
public interface IDotvvmJsonConverter<T>: IDotvvmJsonConverter
{
public T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, DotvvmSerializationState state);
public T Populate(ref Utf8JsonReader reader, Type typeToConvert, T value, JsonSerializerOptions options, DotvvmSerializationState state);
public void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options, DotvvmSerializationState state, bool requireTypeField = true, bool wrapObject = true);
}

Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ namespace DotVVM.Framework.ViewModel.Serialization
{
public interface IViewModelSerializer
{
JsonSerializerOptions ViewModelJsonOptions { get; }
ReadOnlyMemory<byte> BuildStaticCommandResponse(IDotvvmRequestContext context, object? commandResult, string[]? knownTypeMetadata = null);

string SerializeViewModel(IDotvvmRequestContext context, object? commandResult = null, IEnumerable<(string name, string html)>? postbackUpdatedControls = null, bool serializeNewResources = false);
Expand Down

0 comments on commit 61796d5

Please sign in to comment.