Custom JsonFormatter in Utf8Json is ignored - c#

I have this simple JsonFormatter:
public sealed class Int64StringConversionFormatter : IJsonFormatter<long> {
public void Serialize(ref JsonWriter writer, long value, IJsonFormatterResolver formatterResolver) {
writer.WriteString(value.ToString(NumberFormatInfo.InvariantInfo));
}
public long Deserialize(ref JsonReader reader, IJsonFormatterResolver formatterResolver) {
var token = reader.GetCurrentJsonToken();
if (token == JsonToken.String) {
var s = reader.ReadString();
return
long.TryParse(s, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out var l)
? l
: 0;
}
if (token != JsonToken.Number)
throw new ValueProviderException("The provided value is not String or Int64.");
var value = reader.ReadInt64();
return value;
}
}
which is an implementation of Utf8Json.IJsonFormatter<> for working with long values. I've added this formatter to an AspNetCore WebApi application like this:
public static MvcOptions SetupCustomJsonFormatter(
this MvcOptions options) {
CompositeResolver.RegisterAndSetAsDefault(_formatters, _resolvers);
options.InputFormatters.Insert(0, new Utf8JsonInputFormatter());
options.OutputFormatters.Insert(0, new Utf8JsonOutputFormatter());
return options;
}
And here is my _formatters and _resolvers:
static readonly IJsonFormatterResolver[] resolvers = {
StandardResolver.ExcludeNullCamelCase,
ImmutableCollectionResolver.Instance,
EnumResolver.Default,
DynamicGenericResolver.Instance,
};
static readonly IJsonFormatter[] _formatters
= new [] {new Int64StringConversionFormatter()};
Also, here is my Utf8JsonInputFormatter:
internal sealed class Utf8JsonInputFormatter : IInputFormatter {
private readonly IJsonFormatterResolver _resolver;
public Utf8JsonInputFormatter() : this(null) {
}
public Utf8JsonInputFormatter(IJsonFormatterResolver resolver) {
_resolver = resolver ?? JsonSerializer.DefaultResolver;
}
public bool CanRead(InputFormatterContext context)
=> context.HttpContext.Request.ContentType?.StartsWith("application/json") == true;
public async Task<InputFormatterResult> ReadAsync(InputFormatterContext context) {
var request = context.HttpContext.Request;
if (request.Body.CanSeek && request.Body.Length == 0) return await InputFormatterResult.NoValueAsync();
var result = await JsonSerializer.NonGeneric.DeserializeAsync(context.ModelType, request.Body, _resolver);
return await InputFormatterResult.SuccessAsync(result);
}
}
Everything seems to should be OK. But Int64StringConversionFormatter.Serialize and Int64StringConversionFormatter.Deserializ methods never get called. I tested the configuration with another simple formatter (say UnixDateTimeFormatter) and it works just fine. But I cannot figure it out why this one isn't getting called. Do you have any idea what am I missing here?

Related

ModelBinder converting existing values in properties to null

I have a modelbinder like this:
public class CustomQuarantineModelBinder : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.Metadata.ModelType.GetInterfaces().Contains(typeof(IQuarantineControl))
{
return new QuarantineModelBinder();
}
return null;
}
}
public class QuarantineModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext modelBindingContext)
{
char[] delimeter = { '|' };
if (modelBindingContext == null)
{
throw new ArgumentNullException(nameof(modelBindingContext));
}
var model = Activator.CreateInstance(modelBindingContext.ModelType);
if (modelBindingContext.ModelType.GetInterfaces().Contains(typeof(IQuarantineControl)))
{
var qc = model as IQuarantineControl;
if (qc != null)
{
var request = modelBindingContext.HttpContext.Request;
string QuarantineControl = request.Form["QuarantineControl"];
if (!string.IsNullOrEmpty(QuarantineControl))
{
string[] components = QuarantineControl.Split(delimeter);
qc.QuarantineClear();
qc.QuarantineControlID = Convert.ToInt32(components[0]);
qc.QuarantineState = (QuarantineState)Convert.ToInt32(components[1]);
for (int i = 2; i < components.Length; i++)
{
qc.QuarantineReasons.Add(components[i]);
}
}
}
}
modelBindingContext.Result = ModelBindingResult.Success(model);
return Task.CompletedTask;
}
However, other fields in the model is getting turned as null or empty. I would like to set QuarantineState, QuarantineControlId etc.. without affecting other values. Thanks
When your custom IModelBinderProvider returns a binder, that binder is responsible for binding the entire type. If you wish to fall back to the default MVC binder for other properties, you will need to do so explicitly.
Perhaps something like;
public class CustomQuarantineModelBinder : IModelBinderProvider
{
private readonly IModelBinderProvider baseProvider;
public CustomQuarantineModelBinder(IModelBinderProvider baseProvider){
this.baseProvider = baseProvider;
}
public IModelBinder GetBinder(ModelBinderProviderContext context){
...
return new QuarantineModelBinder(BaseProvider.GetBinder(context));
}
}
public class QuarantineModelBinder : IModelBinder
{
private readonly IModelBinder binder;
public QuarantineModelBinder(IModelBinder binder){
this.binder = binder;
}
public Task BindModelAsync(ModelBindingContext modelBindingContext)
{
...
binder.BindModelAsync(modelBindingContext);
...
}
}
services.AddMvc(options =>
{
var baseProvider = options.ModelBinderProviders
.OfType<ComplexObjectModelBinderProvider>()
.First();
options.ModelBinderProviders.Insert(0, new CustomQuarantineModelBinder(baseProvider));
});
When the model object is posted the string values that are empty are converted to null. This is the default behavior of the MVC model binder.
You can try a workaround like this,
public sealed class EmptyStringModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext,
ModelBindingContext bindingContext)
{
bindingContext.ModelMetadata.ConvertEmptyStringToNull = false;
//Binders = new ModelBinderDictionary() { DefaultBinder = this };
return base.BindModel(controllerContext, bindingContext);
}
}

Prevent multicasting in generic method

I am making generic validators for checking input:
Interface:
public interface IInputValidator
{
bool CanHandle<T>();
bool Validate<T>(string? input, out T result);
}
Implementation:
public class IntegerValidator : IInputValidator
{
public bool CanHandle<T>()
{
return typeof(T) == typeof(int);
}
public bool Validate<T>(string? input, out T result)
{
var isValid = int.TryParse(input, out var res);
result = (T)(object)res;
return isValid;
}
}
Then I grab all the validators I have and inject like so: (it feels convenient that the interface itself is not generic thus I don't have to inject them one by one and able to group them in a single collection)
private readonly IEnumerable<IInputValidator> _inputValidators;
public CallerClass(IEnumerable<IInputValidator> inputValidators)
{
_inputValidators = inputValidators;
}
And call it like:
var validator = _inputValidators.First(r => r.CanHandle<int>());
var isInputValid = validator.Validate(userInput, out int id);
It all looks fine except for this line in implementation
result = (T)(object)res;
I feel like something is wrong here but can't figure out how to make it better. It works like this though.
The core issue is that you are trying to combine the resolution of the appropriate validator and the action of that validator into the same generic interface.
If you are willing to separate the resolver and validator functionality into two interfaces:
public interface IInputValidatorResolver
{
bool CanHandle<T>();
IInputValidator<T> GetValidator<T>();
}
public interface IInputValidator<T>
{
bool Validate(string? input, out T result);
}
you can work with IInputValidatorResolver instances in your CallerClass contract to instead resolve the appropriate validator and strongly-type you call to the validator without an object cast. The resolver implementations can create a cache that casts your Validator to a generic IInputValidator<T> instance.
public class IntegerValidatorResolver : IInputValidatorResolver
{
public bool CanHandle<T>() => typeof(T) == typeof(int);
public IInputValidator<T> GetValidator<T>() => Cache<T>.Validator;
private static class Cache<T>
{
public static readonly IInputValidator<T> Validator = BuildValidator();
private static IInputValidator<T> BuildValidator() => ((IInputValidator<T>)new IntegerValidator());
}
}
public class LongValidatorResolver : IInputValidatorResolver
{
public bool CanHandle<T>() => typeof(T) == typeof(long);
public IInputValidator<T> GetValidator<T>() => Cache<T>.Validator;
private static class Cache<T>
{
public static readonly IInputValidator<T> Validator = BuildValidator();
private static IInputValidator<T> BuildValidator() => ((IInputValidator<T>)new LongValidator());
}
}
public class IntegerValidator : IInputValidator<int>
{
public bool Validate(string? input, out int result) => int.TryParse(input, out result);
}
public class LongValidator : IInputValidator<long>
{
public bool Validate(string? input, out long result) => long.TryParse(input, out result);
}
and you can test it with the following:
IEnumerable<IInputValidatorResolver> validatorResolvers = new List<IInputValidatorResolver> { new IntegerValidatorResolver(), new LongValidatorResolver() };
var intValidator = validatorResolvers.First(x => x.CanHandle<int>()).GetValidator<int>();
var isIntValid = intValidator.Validate(long.MaxValue.ToString(), out int intResult);
Console.WriteLine(isIntValid);
Console.WriteLine(intResult);
var longValidator = validatorResolvers.First(x => x.CanHandle<long>()).GetValidator<long>();
var isLongValid = longValidator.Validate(long.MaxValue.ToString(), out long longResult);
Console.WriteLine(isLongValid);
Console.WriteLine(longResult);
That said, this creates an awkward contract where if you do NOT perform a check with CanHandle<T> first, your call to GetValidator<T> can throw an exception. In addition, in either this implementation or your current implementation, you have to loop through resolvers/validators to find the appropriate instance, which is unnecessarily wasteful.
As a result, it may make more sense to have a single IInputValidatorResolver instance that knows how to resolve the appropriate validator based on the type of T, without a CanHandle<T>() check.
public interface IInputValidatorResolver
{
IInputValidator<T> GetValidator<T>();
}
public class ValidatorResolver : IInputValidatorResolver
{
public IInputValidator<T> GetValidator<T>() => Cache<T>.Validator;
private static class Cache<T>
{
public static readonly IInputValidator<T> Validator = BuildValidator();
private static IInputValidator<T> BuildValidator()
{
if (typeof(T) == typeof(int))
{
return ((IInputValidator<T>)new IntegerValidator());
}
else if (typeof(T) == typeof(long))
{
return ((IInputValidator<T>)new LongValidator());
}
else
{
throw new ArgumentException($"{typeof(T).FullName} does not have a registered validator.");
}
}
}
}
public interface IInputValidator<T>
{
bool Validate(string? input, out T result);
}
public class IntegerValidator : IInputValidator<int>
{
public bool Validate(string? input, out int result) => int.TryParse(input, out result);
}
public class LongValidator : IInputValidator<long>
{
public bool Validate(string? input, out long result) => long.TryParse(input, out result);
}
This allows for a much cleaner API and far less registration and enumeration:
IInputValidatorResolver resolver = new ValidatorResolver();
var intValidator = resolver.GetValidator<int>();
var isIntValid = intValidator.Validate(long.MaxValue.ToString(), out int intResult);
Console.WriteLine(isIntValid);
Console.WriteLine(intResult);
var longValidator = resolver.GetValidator<long>();
var isLongValid = longValidator.Validate(long.MaxValue.ToString(), out long longResult);
Console.WriteLine(isLongValid);
Console.WriteLine(longResult);
UPDATE
It looks like you want a purely constructor-injection driven solution. You can accomplish this by registering a new interface as IEnumerable<IInputValidator> and injecting it into the resolver instance.
The IInputValidator interface is responsible for the CanHandle<T>() method that is checked prior to casting to IInputValidator<T>.
public interface IInputValidatorResolver
{
IInputValidator<T> GetValidator<T>();
}
public class ValidatorResolver : IInputValidatorResolver
{
private IEnumerable<IInputValidator?> _validators;
public ValidatorResolver(IEnumerable<IInputValidator?> validators)
{
_validators = validators;
}
public IInputValidator<T> GetValidator<T>()
{
foreach (var validator in _validators)
{
if (validator!.CanHandle<T>())
{
return (IInputValidator<T>)validator;
}
}
throw new ArgumentException($"{typeof(T).FullName} does not have a registered validator.");
}
}
public interface IInputValidator
{
bool CanHandle<TInput>();
}
public interface IInputValidator<T> : IInputValidator
{
bool Validate(string? input, out T result);
}
public class IntegerValidator : IInputValidator<int>
{
public bool CanHandle<T>() => typeof(T) == typeof(int);
public bool Validate(string? input, out int result) => int.TryParse(input, out result);
}
public class LongValidator : IInputValidator<long>
{
public bool CanHandle<T>() => typeof(T) == typeof(long);
public bool Validate(string? input, out long result) => long.TryParse(input, out result);
}
You can test this behavior with the following:
var servicesCollection = new ServiceCollection();
servicesCollection.AddTransient(typeof(IEnumerable<IInputValidator>), s =>
{
return new List<IInputValidator>
{
new IntegerValidator(),
new LongValidator()
};
});
servicesCollection.AddTransient<IInputValidatorResolver, ValidatorResolver>();
var serviceProvider = servicesCollection.BuildServiceProvider();
var resolver = serviceProvider.GetService<IInputValidatorResolver>();
var intValidator = resolver.GetValidator<int>();
var isIntValid = intValidator.Validate(long.MaxValue.ToString(), out int intResult);
Console.WriteLine(isIntValid);
Console.WriteLine(intResult);
var longValidator = resolver.GetValidator<long>();
var isLongValid = longValidator.Validate(long.MaxValue.ToString(), out long longResult);
Console.WriteLine(isLongValid);
Console.WriteLine(longResult);
With this approach, you no longer need the ValidationResolver. It is simply a way to encapsulate the logic of GetValidator<T>. You could just as easily inject IEnumerable<IInputValidator> into your consuming classes and perform the cast each time you need to use it.
Another option is to use Autofac's IComponentContext:
using Autofac;
using TransactionStorage.Interface;
namespace TransactionStorage.Core
{
public class InputResolver : IInputResolver
{
private readonly IComponentContext _context;
public InputResolver (IComponentContext context)
{
_context = context;
}
public bool Validate<T>(string? userInput, out T result) where T : struct
{
var validator = _context.Resolve<IInputValidator<T>>();
return validator.Validate(userInput, out result);
}
}
}

asp.net core Dependency Injection into System.ComponentModel.TypeConverter

I have a service and ILogger that I need to use in my TypeConverter but the constructor with the parameters isn't being used or fired and my logger and service remain null when converting the type.
public class ModelServiceVersionConverter : TypeConverter
{
private static readonly Type StringType = typeof(string);
private static readonly Type VersionType = typeof(ModelServiceVersion);
private readonly ModelServiceVersions modelServiceVersions;
private readonly ILogger<ModelServiceVersionConverter> logger;
public ModelServiceVersionConverter()
{
}
public ModelServiceVersionConverter(ModelServiceVersions modelServiceVersions, ILogger<ModelServiceVersionConverter> logger)
{
this.modelServiceVersions = modelServiceVersions;
this.logger = logger;
}
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) =>
sourceType == StringType ||
sourceType == VersionType ||
base.CanConvertFrom(context, sourceType);
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
if (value is null)
{
return this.modelServiceVersions.ServiceVersions.Last();
}
var modelServiceVersion = this.modelServiceVersions.ServiceVersions.FirstOrDefault(x => x == value.ToString());
if (modelServiceVersion is null)
{
var errorMessage = $"Version {value} unexpected. No implementation of {nameof(IModelService)} for this version.";
this.logger.LogError(errorMessage);
throw new ArgumentException(errorMessage);
}
return modelServiceVersion;
}
}
The ModelServiceVersion class is really simple and has the TypeConverter attribute on it.
[TypeConverter(typeof(ModelServiceVersionConverter))]
public class ModelServiceVersion : ValueObject, IComparer<ModelServiceVersion>
{
private readonly string version;
public static ModelServiceVersion New(string version)
{
return new ModelServiceVersion(version);
}
private ModelServiceVersion(string version) =>
this.version = version;
public static implicit operator string(ModelServiceVersion modelServiceVersion) => modelServiceVersion.version;
public static implicit operator ModelServiceVersion(StringValues stringValues) => New(stringValues[0]);
protected override IEnumerable<object> GetEqualityComponents()
{
yield return this.version;
}
public int Compare(ModelServiceVersion x, ModelServiceVersion y)
{
// TODO these rules can and will likely change
var versionNumberX = FirstOrDefaultDigit(x?.version);
var versionNumberY = FirstOrDefaultDigit(x?.version);
if (y is null || versionNumberY is null)
{
return 1;
}
if (x is null || versionNumberX is null)
{
return -1;
}
if (versionNumberX == versionNumberY)
{
return 0;
}
return versionNumberX > versionNumberY ? 1 : -1;
static int? FirstOrDefaultDigit(string versionString) =>
versionString?.ToCharArray().FirstOrDefault(IsDigit);
}
}
The services are all registered with Microsoft.Extensions.DependencyInjection.IServiceCollection.
I have tried just registering as scoped but it's not firing the type converter constructor to inject the dependencies in.
services.AddScoped<ModelServiceVersionConverter>();
When being used in the action on the controller
public async Task<IActionResult> GetPrediction([FromRoute] int decisionModelId, [FromQuery] ModelServiceVersion version, [FromBody] GetPredictionModelRequest.Request request)
{
var result = await this.predictionModelQueries.GetPrediction(new GetPredictionModelRequest(decisionModelId, request));
return this.Ok(result);
}
Any ideas where I'm going wrong?
Any help greatly appreciated.
Let's assume you have created a custom Attribute and applied it to a method or a class. Will constructor DI work in this case?
No. This is not supported unless you use specific asp.net-core attributes like ServiceFilterAttribute.
So you need to redesign your classes or have static access to DI container IServiceProvider.

Asp net core rc2. Abstract class model binding

In the RC1 I use the following code for abstract classes or interfaces binding:
public class MessageModelBinder : IModelBinder {
public Task<ModelBindingResult> BindModelAsync(ModelBindingContext bindingContext) {
if(bindingContext.ModelType == typeof(ICommand)) {
var msgTypeResult = bindingContext.ValueProvider.GetValue("messageType");
if(msgTypeResult == ValueProviderResult.None) {
return ModelBindingResult.FailedAsync(bindingContext.ModelName);
}
var type = Assembly.GetAssembly(typeof(MessageModelBinder )).GetTypes().SingleOrDefault(t => t.FullName == msgTypeResult.FirstValue);
if(type == null) {
return ModelBindingResult.FailedAsync(bindingContext.ModelName);
}
var metadataProvider = (IModelMetadataProvider)bindingContext.OperationBindingContext.HttpContext.RequestServices.GetService(typeof(IModelMetadataProvider));
bindingContext.ModelMetadata = metadataProvider.GetMetadataForType(type);
}
return ModelBindingResult.NoResultAsync;
}
}
This binder only reads model type (messageType parameter) from query string and overrides metadata type. And the rest of the work performed by standard binders such as BodyModelBinder.
In Startup.cs I just add first binder:
services.AddMvc().Services.Configure<MvcOptions>(options => {
options.ModelBinders.Insert(0, new MessageModelBinder());
});
Controller:
[Route("api/[controller]")]
public class MessageController : Controller {
[HttpPost("{messageType}")]
public ActionResult Post(string messageType, [FromBody]ICommand message) {
}
}
How can I perform this in RC2?
As far as I understand, now I have to use IModelBinderProvider. OK, I tried.
Startup.cs:
services.AddMvc().Services.Configure<MvcOptions>(options => {
options.ModelBinderProviders.Insert(0, new MessageModelBinderProvider());
});
ModelBinderProvider:
public class MessageModelBinderProvider : IModelBinderProvider {
public IModelBinder GetBinder(ModelBinderProviderContext context) {
if(context == null) {
throw new ArgumentNullException(nameof(context));
}
return context.Metadata.ModelType == typeof(ICommand) ? new MessageModelBinder() : null;
}
}
ModelBinder:
public class MessageModelBinder : IModelBinder {
public Task BindModelAsync(ModelBindingContext bindingContext) {
if(bindingContext.ModelType == typeof(ICommand)) {
var msgTypeResult = bindingContext.ValueProvider.GetValue("messageType");
if(msgTypeResult == ValueProviderResult.None) {
bindingContext.Result = ModelBindingResult.Failed(bindingContext.ModelName);
return Task.FromResult(0);
}
var type = typeof(MessageModelBinder).GetTypeInfo().Assembly.GetTypes().SingleOrDefault(t => t.FullName == msgTypeResult.FirstValue);
if(type == null) {
bindingContext.Result = ModelBindingResult.Failed(bindingContext.ModelName);
return Task.FromResult(0);
}
var metadataProvider = (IModelMetadataProvider)bindingContext.OperationBindingContext.HttpContext.RequestServices.GetService(typeof(IModelMetadataProvider));
bindingContext.ModelMetadata = metadataProvider.GetMetadataForType(type);
bindingContext.Result = ModelBindingResult.Success(bindingContext.ModelName, Activator.CreateInstance(type));
}
return Task.FromResult(0);
}
}
But I cannot specify NoResult. If I do not specify bindingContext.Result, I get null model in controller.
If I specify bindingContext.Result, I get empty model without setting model fields.
I had a similar requirement with custom model binding and abstract classes and the suggestions posted by dougbu on github AspNet/Mvc/issues/4703 worked for me. I upgraded from RC1 to ASP.NET Core 1.0 and needed to modify my custom model binder with his recommendations. I've copy & pasted his code below in case the link to the github issue breaks. Read the comments in the github issue for security considerations around code that creates objects of a requested type on the server.
MessageModelBinderProvider
public class MessageModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.Metadata.ModelType != typeof(ICommand))
{
return null;
}
var binders = new Dictionary<string, IModelBinder>();
foreach (var type in typeof(MessageModelBinderProvider).GetTypeInfo().Assembly.GetTypes())
{
var typeInfo = type.GetTypeInfo();
if (typeInfo.IsAbstract || typeInfo.IsNested)
{
continue;
}
if (!(typeInfo.IsClass && typeInfo.IsPublic))
{
continue;
}
if (!typeof(ICommand).IsAssignableFrom(type))
{
continue;
}
var metadata = context.MetadataProvider.GetMetadataForType(type);
var binder = context.CreateBinder(metadata);
binders.Add(type.FullName, binder);
}
return new MessageModelBinder(context.MetadataProvider, binders);
}
}
MessageModelBinder
public class MessageModelBinder : IModelBinder
{
private readonly IModelMetadataProvider _metadataProvider;
private readonly Dictionary<string, IModelBinder> _binders;
public MessageModelBinder(IModelMetadataProvider metadataProvider, Dictionary<string, IModelBinder> binders)
{
_metadataProvider = metadataProvider;
_binders = binders;
}
public async Task BindModelAsync(ModelBindingContext bindingContext)
{
var messageTypeModelName = ModelNames.CreatePropertyModelName(bindingContext.ModelName, "messageType");
var messageTypeResult = bindingContext.ValueProvider.GetValue(messageTypeModelName);
if (messageTypeResult == ValueProviderResult.None)
{
bindingContext.Result = ModelBindingResult.Failed();
return;
}
IModelBinder binder;
if (!_binders.TryGetValue(messageTypeResult.FirstValue, out binder))
{
bindingContext.Result = ModelBindingResult.Failed();
return;
}
// Now know the type exists in the assembly.
var type = Type.GetType(messageTypeResult.FirstValue);
var metadata = _metadataProvider.GetMetadataForType(type);
ModelBindingResult result;
using (bindingContext.EnterNestedScope(metadata, bindingContext.FieldName, bindingContext.ModelName, model: null))
{
await binder.BindModelAsync(bindingContext);
result = bindingContext.Result;
}
bindingContext.Result = result;
}
}

Serializing linked objects

I try to create an object model for the following problem.
I need a folder object (comparable to directory folders). Each folder can contain additional sub folders and in addition parameter objects (comparable to files). In addition, each parameter needs to know in which folder it resides. This is easy so far. So I implemented the following working solution.
I have a base object, that can either be inherited to a folder or a parameter:
[Serializable()]
public class Entry
{
public Func<string> GetPath;
public string Path
{
get
{
if (GetPath == null) return string.Empty;
return GetPath.Invoke();
}
}
}
Now I created a FolderEntry, that inherits from Entry and supports adding new sub entries by implementing IList<>.
[Serializable()]
class FolderEntry : Entry, IList<Entry>
{
private readonly List<Entry> _entries;
public FolderEntry()
{
_entries = new List<Entry>();
}
public string FolderName { get; set; }
private void SetPathDelegate(Entry entry)
{
if (entry.GetPath != null) throw new ArgumentException("entry already assigned");
entry.GetPath = () =>
{
if (GetPath == null || string.IsNullOrEmpty(GetPath.Invoke())) return FolderName;
return GetPath.Invoke() + "|" + FolderName;
};
}
public void Add(Entry item)
{
SetPathDelegate(item);
_entries.Add(item);
}
[...]
}
To support Undo/Redo functionality, I made all classes serializable by adding the Serializable-Attribute.
This serialization is working so far using the following test:
var folderA = new FolderEntry();
var folderB = new FolderEntry();
folderA.Add(folderB);
var serializer = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
var memStream = new System.IO.MemoryStream();
serializer.Serialize(memStream, folderA);
Now here’s my problem. There is in addition the need that each parameter knows its index inside the hosting list. I changed my Entry-object to have a property Index and a delegate GetIndex in the same manner as Path and GetPath before:
[Serializable()]
public class Entry
{
public Func<string> GetPath;
public string Path
{
get
{
if (GetPath == null) return string.Empty;
return GetPath.Invoke();
}
}
public Func<int> GetIndex;
public int Index
{
get
{
if (GetIndex == null) return -1;
return GetIndex.Invoke();
}
}
}
Inside the SetPathDelegate of the Folder-object I assigned the new delegate
private void SetPathDelegate(Entry entry)
{
if (entry.GetPath != null) throw new ArgumentException("entry already assigned");
if (entry.GetIndex != null) throw new ArgumentException("entry already assigned");
entry.GetPath = () =>
{
if (GetPath == null || string.IsNullOrEmpty(GetPath.Invoke())) return FolderName;
return GetPath.Invoke() + "|" + FolderName;
};
entry.GetIndex = () =>
{
return _entries.IndexOf(entry);
};
}
If I try to serialize this, I get an expection that my „FolderEntry+<>c__DisplayClass2“ in Assembly… is not marked as serializable. I can’t see an obvious difference between GetPath and GetIndex. To narrow it down, I replaced content of the created GetIndex delegate in SetPathDelegate from
entry.GetIndex = () =>
{
return _entries.IndexOf(entry);
};
To
entry.GetIndex = () =>
{
return -1;
};
To my astonishment this is serializable again. Why doesn‘t cause my GetPath delegate any problems regarding the serialization but my GetIndex delegate does?
The problem is the anonymous function that you assign to GetIndex. At runtime, a new type is created, which is not marked as serializable.
According to this post, you should set a SurrogateSelector for the formatter (with some caveats, read the article in detail):
formatter.SurrogateSelector = new UnattributedTypeSurrogateSelector();
I'me pasting here the classes from the article, for future reference and in order to make the answer thorough.
public class UnattributedTypeSurrogate : ISerializationSurrogate
{
private const BindingFlags publicOrNonPublicInstanceFields =
BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public;
public void GetObjectData(object obj,
SerializationInfo info, StreamingContext context)
{
var type = obj.GetType();
foreach (var field in type.GetFields(publicOrNonPublicInstanceFields))
{
var fieldValue = field.GetValue(obj);
var fieldValueIsNull = fieldValue != null;
if (fieldValueIsNull)
{
var fieldValueRuntimeType = fieldValue.GetType();
info.AddValue(field.Name + "RuntimeType",
fieldValueRuntimeType.AssemblyQualifiedName);
}
info.AddValue(field.Name + "ValueIsNull", fieldValueIsNull);
info.AddValue(field.Name, fieldValue);
}
}
public object SetObjectData(object obj,
SerializationInfo info, StreamingContext context, ISurrogateSelector selector)
{
var type = obj.GetType();
foreach (var field in type.GetFields(publicOrNonPublicInstanceFields))
{
var fieldValueIsSerializable = info.GetBoolean(field.Name + "ValueIsNull");
if (fieldValueIsSerializable)
{
var fieldValueRuntimeType = info.GetString(field.Name + "RuntimeType");
field.SetValue(obj,
info.GetValue(field.Name, Type.GetType(fieldValueRuntimeType)));
}
}
return obj;
}
}
public class UnattributedTypeSurrogateSelector : ISurrogateSelector
{
private readonly SurrogateSelector innerSelector = new SurrogateSelector();
private readonly Type iFormatter = typeof(IFormatter);
public void ChainSelector(ISurrogateSelector selector)
{
innerSelector.ChainSelector(selector);
}
public ISerializationSurrogate GetSurrogate(
Type type, StreamingContext context, out ISurrogateSelector selector)
{
if (!type.IsSerializable)
{
selector = this;
return new UnattributedTypeSurrogate();
}
return innerSelector.GetSurrogate(type, context, out selector);
}
public ISurrogateSelector GetNextSelector()
{
return innerSelector.GetNextSelector();
}
}

Categories

Resources