1

When doing an EF data migration, we have a bunch of json with pre set guids like this:

  {
    "Id": "61dcc24e9b524f10b69a5c3f17be8603",
    "MakeName": "AUDI",
    "ExternalId": "61dcc24e9b524f10b69a5c3f17be8604",
    "CreatedBy": "System",
    "CreatedOn": "2022/01/05"
  },
  {
    "Id": "27a617d75b2e45bab513e2f336fcd921",
    "MakeName": "BMW",
    "ExternalId": "27a617d75b2e45bab513e2f336fcd927",
    "CreatedBy": "System",
    "CreatedOn": "2022/01/05"
  },

Make Class

 public class Make : AuditableEntity
    {
        public Make() { }

        Guid Id { get; }

        public String MakeName { get; set; }

        public String CreatedBy { get; set; } = null!;

        public DateTimeOffset CreatedOn { get; set; }

        Guid ExternalId{ get; set; }
    }

We then use a generic seed function to extract the data:

public static List<TEntity> SeedFromJson<TEntity>(string fileName)
        {
            string path = "../path/Seeds";
            string fullPath = Path.Combine(path, fileName);

            var result = new List<TEntity>();
            using (StreamReader reader = new StreamReader(fullPath))
            {
                string json = reader.ReadToEnd();
                result = JsonConvert.DeserializeObject<List<TEntity>>(json);
            }

            return result;
        }

The result is used in a custom config like this:

builder.HasData(LargeDataHelper.SeedFromJson<Make>("Makes.json"));

The issue is that when we run the migration. The following error is shown:

The seed entity for entity type 'Make' cannot be added because a default value was provided for the required property 'Id'. Please provide a value different from '00000000-0000-0000-0000-000000000000'.

It seems like when debugging the result = JsonConvert.DeserializeObject replaces the guid provided with zeros.

Bike_dotnet
  • 234
  • 6
  • 18
  • Provide DTO class used as TEntity. – eocron Jan 05 '22 at 07:44
  • 5
    You don't have the guid in correct format in json. – Chetan Jan 05 '22 at 07:46
  • Correct format is D, you are instead using N. – eocron Jan 05 '22 at 07:47
  • I bet your ids are being saved on database from not dotnet framework. When you stored id in dotnet stack, you get the id in a format that presents several "-" – Alberto León Jan 05 '22 at 07:54
  • I tried D format as well, Same error – Bike_dotnet Jan 05 '22 at 07:59
  • 2
    Also your `Id` is not `public` and does not have a setter. Is this a mistake? – lordvlad30 Jan 05 '22 at 08:02
  • @lordvlad30 So this is all intent generated. So its something that is generated as is :( – Bike_dotnet Jan 05 '22 at 08:04
  • There is a way for such cases. You can write your own converter for this field. Here is example for null-handling, I think it will not be hard to adapt it to Guid.ToString("N") case: https://stackoverflow.com/questions/31747712/json-net-deserialization-null-guid-case/31750851 – eocron Jan 05 '22 at 08:07
  • @Bike_dotnet then change the generated code even if it is just to try, is that not possible? change to -> `public Guid Id { get; set; }` – lordvlad30 Jan 05 '22 at 08:08
  • 2
    There is already such a question: https://stackoverflow.com/questions/56234561/control-guid-format-in-asp-net-core-actions-response – Givko Jan 05 '22 at 08:08
  • 1
    Does this answer your question? [Control Guid format in ASP.NET Core action's response](https://stackoverflow.com/questions/56234561/control-guid-format-in-asp-net-core-actions-response) – Givko Jan 05 '22 at 08:09
  • Those dups are similar but this is _NOT_ to do with the action response, this is specifically a _deserialization_ issue, complicated by private and readonly members... which requires a slightly different approach, or OP must change the model – Chris Schaller Jan 05 '22 at 09:35

1 Answers1

5

The zeros is the default state of a Guid that is not nullable, so the value of new Guid() or Guid.Empty

That means that the deserialisation has failed or not been attempted for this property

There are 3 main reasons for this:

  1. The Id and ExternalId properties are private and so cannot be accessed from the JsonConvert process by default

  2. The Id property is readonly, so the deserialization process cannot write to it, you. you should make it writable by adding a setter.

  3. The Guid values are not in the correct format, in .Net the serialised structure for a Guid should look like this

{
    "Id": "9694e5ec-9818-4987-abd8-6848132d3e4c",
}

The value parsing can be solved with a simple JsonConverter as shown in this fiddle: https://dotnetfiddle.net/KKYTUT

public class GuidConverter : JsonConverter<Guid>
{
    public GuidConverter()
    {
    }

    public override void WriteJson(JsonWriter writer, Guid value, JsonSerializer serializer)
    {
        writer.WriteValue(value.ToString("N"));
    }

    public override Guid ReadJson(JsonReader reader, Type objectType, Guid existingValue, bool hasExistingValue, JsonSerializer serializer)
    {
        string value = reader.Value.ToString();
        return Guid.Parse(value);
    }

    public override bool CanRead
    {
        get { return true; }
    }

}

Then change the model to support this converter, by making the properties public and writable:

public class Make 
{
    public Make() {}
    [JsonConverter(typeof(GuidConverter))]
    public Guid Id { get; set; }
    public String MakeName { get; set; }
    public String CreatedBy { get; set; }
    public DateTimeOffset CreatedOn { get; set; }
    [JsonConverter(typeof(GuidConverter))]
    public Guid ExternalId{ get; set; }
}

That will work with your current SeedFromJson method, but will also affect the serialization of this type as well.

If it is really necessary to maintain the private access modifiers on the values and the readonly state of the Id, then we need to write a custom type converter for this type or may need to add additional structure changes to support this.

A simple way to do this might be to use the OnDeserilized callback in conjunction with the ExtensionData attribute, this will capture the Json tokens that failed to deserialize and we can reference them at the end of the process.

This fiddle shows the implementation: https://dotnetfiddle.net/sA7ZQl

The added bonus to this implementation is that it requires no modifications or dependencies outside of this class and it only affects Deserilization, the default .Net Serialization and therefor the responses from your controllers will be unaffected.

Note that the Id property has been re-implemented with a backing field, this is so we can set the value outside of the constructor.

public class Make
{
    public Make() { }
    private Guid Id { get { return _Id; } }
    private Guid _Id;
    public String MakeName { get; set; }
    public String CreatedBy { get; set; }
    public DateTimeOffset CreatedOn { get; set; }
    private Guid ExternalId { get; set; }

    [JsonExtensionData]
    private IDictionary<string, JToken> _additionalData;

    [System.Runtime.Serialization.OnDeserialized]
    private void OnDeserialized(System.Runtime.Serialization.StreamingContext context)
    {
        // Id and ExternalId are not public, so we capture the values into the Extension data to simplify psot processing
        if (_additionalData.TryGetValue("Id", out JToken @id))
            this._Id = Guid.Parse(id.ToString());
        if (_additionalData.TryGetValue("ExternalId", out JToken @ExternalId))
            this.ExternalId = Guid.Parse(@ExternalId.ToString());
    }
}

Also note in the OnDeserilized method that we are checking for the existance of the properties before we use them, there are no gaurantees that the caller will provide them so it is reasonable to check first.

Chris Schaller
  • 13,704
  • 3
  • 43
  • 81