Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ModelBinding fails with Error ... Patient is abstract .... could not be instantiated and populated #2583

Closed
Brilbroeder opened this issue Sep 6, 2023 · 9 comments · Fixed by #2651

Comments

@Brilbroeder
Copy link

project-file
<PackageReference Include="Hl7.Fhir.R5" Version="5.3.0" />

startup.cs

            services
                .AddControllers()
                .AddJsonOptions(options =>
                {
                    options.JsonSerializerOptions.ForFhir(ModelInfo.ModelInspector);
                });

controller-action

        [HttpPost]
        public ActionResult Create(Patient patient)
        {
            return Ok(patient);
        }

Error :
System.NotSupportedException: The collection type 'Hl7.Fhir.Model.Patient' is abstract, an interface, or is read only, and could not be instantiated and populated. Path: $ | LineNumber: 0 | BytePositionInLine: 1.

Any suggestions ?

@ewoutkramer
Copy link
Member

The text "The collection type" is suspicous. It's not a collection.,,,,,wait a second....

Yes, every resource is an IEnumerable<KeyValuePair<>>.

I guess this means that in the list of converters installed in the service, there's a (default) converter installed that serializes collections BEFORE our own special FHIR serializer is called. Which is entirely possible. ForFhir() just adds our converter at the end of the list.

That's probably it. Not yet a solution, but we could start to experiment by wiping the options.Converters array in the lambda before the call to ForFhir()?

@ewoutkramer
Copy link
Member

I do regret letting Base (and subclasses) implement IReadOnlyDictionary<> directly. We've seen a few frameworks thrown off by this, much like the serializer here. Also, for example, the Visual Studio debugger shows resources as key value pairs in its enumerable viewer.

Could we explore the idea of having an indirect implementation, i.e. having an AsReadOnlyDictionary() on Base, instead of it implementing it directly? That's obviously a breaking change, but would it be worth it?

@Brilbroeder
Copy link
Author

@ewoutkramer I tried the following. (see below) This should put your converter first. I put a breakpoint to see the number of savedConverters. It is 0. Hence this did not change anything.

                .AddJsonOptions(options =>
                {
                    var savedConverters = options.JsonSerializerOptions.Converters.ToList();
                    options.JsonSerializerOptions.Converters.Clear();
                    options.JsonSerializerOptions.ForFhir(ModelInfo.ModelInspector);

                    foreach (var savedConverter in savedConverters)
                    {
                        options.JsonSerializerOptions.Converters.Add(savedConverter);
                    }
                });

@ewoutkramer
Copy link
Member

Ok, so that would have been too easy ;-) Still, it's clear to me that our converter, which should handle all resources, is not called, but some more generic converter now handles it, and throws this error.

@ewoutkramer
Copy link
Member

ewoutkramer commented Sep 7, 2023

I found it.

This is what our code in ForFhir() does:

  /// <summary>
  /// Initialize the options to serialize using the JsonFhirConverterFactory, producing compact output without whitespace.
  /// </summary>
  public static JsonSerializerOptions ForFhir(this JsonSerializerOptions options, FhirJsonConverterFactory converter)
  {
      var result = new JsonSerializerOptions(options)
      {
          Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
      };

      result.Converters.Add(converter);
      return result;
  }

In other words, we don't modify the original options passed in, but we create a new options instance, based on the original one, add our converter to it, and then return the new options. But that's not what the service configurator needs: we need to modify the original options, as the return value of ForFhir() is ignored.

@ewoutkramer
Copy link
Member

ewoutkramer commented Sep 7, 2023

If I am correct, then a workaround could be to capture the results of ForFhir(), and then add the converters back into the original. more or less like so:

 .AddJsonOptions(options =>
                {
                    var fhirConverters = options.ForFhir(ModelInfo.ModelInspector).Converters;               
                    foreach (var fhirConverter in fhirConverters)
                    {
                        options.JsonSerializerOptions.Converters.Add(fhirConverter);
                    }

                   options.Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,

@Brilbroeder
Copy link
Author

This workaround is working:

                .AddJsonOptions(options =>
                {
                    var fhirConverters = options.JsonSerializerOptions.ForFhir(ModelInfo.ModelInspector).Converters;
                    foreach (var fhirConverter in fhirConverters)
                    {
                        options.JsonSerializerOptions.Converters.Add(fhirConverter);
                    }
                    options.JsonSerializerOptions.Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping;
                });

Will there be a fix in the SDK ?

@ewoutkramer ewoutkramer added the bug label Sep 9, 2023
@ewoutkramer
Copy link
Member

ewoutkramer commented Sep 9, 2023

Yes, for sure. With this confirmed, we can fix it. I have given this issue the bug label now.

@ridafatima90
Copy link

I believe it should be something like this, we should create new converter, otherwise it is throwing error original Collection was modified error.

.AddJsonOptions(opt =>
{
    IList<JsonConverter> fhirConverters = opt.JsonSerializerOptions.ForFhir(ModelInfo.ModelInspector).Converters;
    IList<JsonConverter> convertersToAdd = new List<JsonConverter>(fhirConverters);
    foreach(JsonConverter fhirConverter in convertersToAdd)
    {
        opt.JsonSerializerOptions.Converters.Add(fhirConverter);
    }
    opt.JsonSerializerOptions.Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping;
});

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants