GithubHelp home page GithubHelp logo

weingartner / migrations.json.net Goto Github PK

View Code? Open in Web Editor NEW
29.0 5.0 5.0 155.03 MB

A simple framework for data migrations using Newtonsoft Json.Net.

License: MIT License

PowerShell 1.73% C# 95.45% Batchfile 0.07% F# 2.76%
json c-sharp serialization dotnet migrations

migrations.json.net's Introduction

Migrations.Json.Net

A simple framework for data migrations using Newtonsoft Json.Net.

Quickstart

Suppose you have the following class that gets serialized in your project:

  class MyData
  {
        string firstName = "John";
        string lastName = "Doe";
  }

But in a later revision, you change it to this:

  class MyData
  {
        string name = "John Doe";
  }

Oh noes! It's not backwards compatible. Well, with this library, you simply have to apply the following modifications:

  [Migratable("")]
  class MyData
  {
        string name = "John Doe";
        
        private static JObject Migrate_1(JObject data, JsonSerializer serializer)
	{
		data["name"] = data["firstName"] + " " + data["lastName"];
		return data;
	}
  }

And poof! When deserializing a json file with the previous format, it will automatically concatenate the previous firstName and lastName fields into the new 'name' field.

You do need to specify to Json.NET that you want some custom behavior to happen while deserializing:

string myData = ...; // Read from file?
var serializerSettings = new JsonSerializerSettings();
serializerSettings.Converters.Add(new MigrationConverter(new HashBasedDataMigrator<JToken>(new JsonVersionUpdater())));
MyData myDataDeserialized = JsonConvert.DeserializeObject<MyData>(myData, serializerSettings);

But that's all! If you want, you can stop reading.

Understanding the basics

Let's explain a bit what happens here.

  [Migratable("")]

This attribute marks the MyData class wrt. the migration system, so that it knows it needs to search for a migration method. The migration system does not parse classes that do not have this attribute.

  private static JObject Migrate_1(JObject data, JsonSerializer serializer)

The signature of the migration method should follow a specific format. First, it should be a static method on the class. It should also have the types defined above for its return type and arguments. Finally, its name should follow the following pattern: Migrate_number, where number is replaced by the version number of this class. A version number of 0 means that it's the original version of this class, and that there is no migration method necessary (hence the name "Migrate_0" is not possible). A version number greater than 0, e.g. n, means that the method is executed to migrate serialized data from version n - 1 to version n.

data["name"] = data["firstName"] + " " + data["lastName"];

Inside the method, you can modify the JObject that represents the deserialized data that comes from version n - 1 so that it can automatically be read by version n. In the example above, data would contain something that corresponds to this:

{
	"firstName": "John",
	"lastName": "Doe"
}

And this in the new format, what you want is this:

{
	"name": "John Doe"
}

That's why you need to add a "name" field to the data object, setting its value to the concatenated values of firstName and lastName.

NB: If you paid attention, you may have realized that the code above actually produces this:

{
	"name": "John Doe",
	"firstName": "John",
	"lastName": "Doe"
}

Indeed, we are not removing the existing "firstName" and "lastName" fields from the data object. But it doesn't matter! Because by default, Json.NET simply ignores json fields that do not appear in the deserialized class. If you are deserializing with a setting that makes Json.NET treat this as an error, you will want to remove those from the data object in the migration method.

Versioning

Each class has its own version field that is used by the migration library to know when to invoke which migration method. If a class doesn't define such a version field, the migration system will consider the class to be of version 0 - so it will invoke all the migration methods in order.

It means that in most cases, you will want to add this version field to your serialized classes. It takes the following form:

class MyData
{
	int Version = 1;
}

So an integer-typed member variable whose name is "Version".

The migration system will automatically increment the version field when deserializing. Let's take our initial example, but now adding the Version field:

  [Migratable("")]
  class MyData
  {
	int Version = 1;
	string name = "John Doe";
        
	private static JObject Migrate_1(JObject data, JsonSerializer serializer)
	{
		data["name"] = data["firstName"] + " " + data["lastName"];
		return data;
	}
  }

If this class is used to deserialize the following data:

{
	"firstName": "John",
	"lastName": "Doe"
}

then the result would be this:

{
	"Version": 1
	"name": "John Doe",
	"firstName": "John",
	"lastName": "Doe"
}

But it would be the same result if the loaded data had a Version field of value 0, e.g.:

{
	"Version": 0,
	"firstName": "John",
	"lastName": "Doe"
}

Because the migration method Migrate_1 would have parsed the Version field and incremented it to 1.

It follows that the Version field should be incremented each time your class is modified with changes that are backwards incompatible (otherwise you need to know what you're doing). Basically whenever you have the need to add a migration method, also remember to increment the Version field to the highest suffix of the migration methods. For instance, if you have the following migration methods: Migrate_1, Migrate_2, Migrate_3, then the Version field should have 3 as its default value, so that new instances of that class have the correct version number and do not invoke incorrect migration methods upon deserialization.

Advanced usage

This library supports nested classes as well, out of the box. Simply remember to mark each class as Migratable and to add a Version field to each class. Example:

  [Migratable("")]
  class Person
  {
	int Version = 1;
	string name = "John Doe";
	Address address = new Address();
        
	private static JObject Migrate_1(JObject data, JsonSerializer serializer)
	{
		data["name"] = data["firstName"] + " " + data["lastName"];
		return data;
	}
  }

  [Migratable("")]
  class Address
  {
	int Version = 1;
	string streetName = "Champs-Élysées";
        
	private static JObject Migrate_1(JObject data, JsonSerializer serializer)
	{
		data["streetName"] = data["street"]; // the field 'street' was renamed to 'streetName'
		return data;
	}
  }

You can also define a custom migrator class for a given class, which lets you define the migration methods in a separate place. Example:

[Migratable(""), CustomMigrator(typeof(MyDataMigrator))]
class MyData
{
	int Version = 1;
	string name = "John Doe";
}
  
class MyDataMigrator 
{
  		private static JObject Migrate_1(JObject data, JsonSerializer serializer)
	{
		data["name"] = data["firstName"] + " " + data["lastName"];
		return data;
	}
}

The automatic tests in this repository demonstrate various use cases, it can be an additional source of inspiration if this readme is not explicit enough.

migrations.json.net's People

Contributors

bradphelan avatar fkorsa avatar florianauinger avatar jkronberger avatar mathistagames avatar samirem avatar wg-bot avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

migrations.json.net's Issues

Support nested migratable types

In WeinCad, a GearCorrectionConfiguration has a property of type ImmutableSortedSet<ControlPoint>. Both GearCorrectionConfiguration and ControlPoint are migratable. IMigrateData should support migrating nested migratable types.

Custom migrator

I saw in a test file a hint that having a "custom migrator" was possible, i.e. define another class that contains the migration methods for a given class.

However I didn't find the API to do that, and the test file did not have any tests that showed how to use it.

Is that API already here, or is that planned for the future?

Would be really handy to be able to define the migration methods in another class, so as not to pollute the business logic.

Version field doesn't work with inheritance

I'm trying to support migrations for a collection of classes that inherit from each other.

If a class with migrations inherits from another class with migrations, currently they both try and use the same version number. Unless I am missing something, it doesn't seem possible to have migrations for both a parent and child class.

Thinking about solutions, it seems like adding custom version name would allow a workaround where each class can specify a different version number.

The code would look as follows:

[Migratable("", "VersionClassA")]
class ClassA
{
    public int VersionClassA = 6;
    public double PropertyA { get; set; }
}

[Migratable("", "VersionClassB")]
class ClassB : ClassA
{
    public int VersionClassB = 3;
    public int PropertyB { get; set; }
}

[Migratable("", "VersionClassC")]
class ClassC : ClassB
{
    public int VersionClassC = 7;
    public string PropertyC { get; set; }
}

Corresponding JSON would have all version numbers:

{
  "VersionClassA": 4,
  "VersionClassB": 1,
  "VersionClassC": 5,
  "PropertyA": 0.555,
  "PropertyB": 100,
  "PropertyC": "foo"
}

Populating object that has read-only getter doesn't work

[DataContract, Migratable("...")]
public class RuleTrigger : ReactiveObject
{
    [Reactive, DataMember] public string A { get; set; }
}

[DataContract]
public class SteadyRule : ReactiveObject
{
    [DataMember] public RuleTrigger RuleTrigger { get; } = new RuleTrigger();
}

var target = new SteadyRule();
target.RuleTrigger.A = 10;
JsonConvert.PopulateObject("{ \"RuleTrigger\": { \"A\": 5 } }", target, SerializerSettings);
target.RuleTrigger.A.Should().Be(5); // is `10`

Migration breaks when using PreserveReferencesHandling

I can't seem to figure out how to resolve object references in a migration method if PreserveReferencesHandling was turned on during serialization. In that case the migrating object's JObject data may have a reference to the object (ex. "$ref": "123") instead of the actual JSON data. Is there a good solution for passing along all the object references from the top of the object graph?

Update: I'm pretty sure this is due to a reference loop. I noticed that MigrationConverterSpec.ShouldMigrateTypesWithReferenceLoops() unit test is skipped because "Not a requirement by now".

Any chance you might have some hints to get me started with a PR to add that?

Error when deserializing with PreserveReferencesHandling.Objects

I'm trying to deserialize with the setting PreserveReferencesHandling = PreserveReferencesHandling.Objects.

Getting the following exception:
Newtonsoft.Json.JsonSerializationException: 'Additional content found in JSON reference object. A JSON reference object should only have a $ref property. Path 'Version'.'

I'm guessing that a Version-tag is inserted in each JObject, even when it is a $ref-object.

Migration method verification is broken

HashBasedDataMigrator::TryMigrate is broken

image


public Tuple<TSerializedData,bool> TryMigrate(TSerializedData serializedData, Type unserializedDataType, JsonSerializer serializer)
{
    if (serializedData == null) throw new ArgumentNullException(nameof(serializedData));
    if (unserializedDataType == null) throw new ArgumentNullException(nameof(unserializedDataType));

    var migrationSettings = unserializedDataType.GetTypeInfo().GetCustomAttribute<MigratableAttribute>();
    if (migrationSettings == null) return Tuple.Create(serializedData, false);


    var version = _VersionExtractor.GetVersion(serializedData);

    var allMigrationMethods = VersionMemberName
        .GetMigrationMethods(unserializedDataType)
        .ToList();

    var maxSupportedVersion = allMigrationMethods.LastOrDefault()?.ToVersion ?? 0;

    if ( version > maxSupportedVersion )
    {
        throw new DataVersionTooHighException($"Trying to load data type '{unserializedDataType.FullName}' from json data " +
                                              $"at version {version}." +
                                              $" However current software only supports version {maxSupportedVersion}." +
                                              " Please update your installation with a newwer version.");
    }

    var migrationMethods = allMigrationMethods
        .SkipWhile(m => m.ToVersion <= version)
        .ToList();

    var migrated = migrationMethods.Count > 0;

    try
    {
        serializedData = migrationMethods
            .Select(m => unserializedDataType.GetTypeInfo().GetDeclaredMethod(m.Name))
            .Aggregate(serializedData,
                (data, method) => ExecuteMigration(method, data, serializer));
    }
    catch
    {
        var migrationMethodVerifier = new MigrationMethodVerifier(VersionMemberName.CanAssign);

        var invalidMethod = migrationMethodVerifier.VerifyMigrationMethods(migrationMethods);
        foreach (var method in invalidMethod)
        {
            method.ThrowIfInvalid();
        }

        // Exception doesn't come from invalid migration methods -> rethrow

        throw;
    }

    _VersionExtractor.SetVersion(serializedData, maxSupportedVersion);

    return Tuple.Create(serializedData, migrated);
}

Note that the line

  var invalidMethod = migrationMethodVerifier.VerifyMigrationMethods(migrationMethods);

is run after the list is filtered by the current version

            var migrationMethods = allMigrationMethods
                .SkipWhile(m => m.ToVersion <= version)
                .ToList();

and in the verification method we have

        public IEnumerable<VerificationResult> VerifyMigrationMethods(IReadOnlyList<MigrationMethod> migrationMethods)
        {
            var firstVersion = migrationMethods.FirstOrDefault()?.ToVersion ?? 1;
            if (firstVersion != 1)
                yield return new VerificationResult(migrationMethods[0], VerificationResultEnum.DoesntStartWithOne, null);

            var nonConsecutive = migrationMethods
                .Zip(migrationMethods.Skip(1), (x, y) => new { Previous = x, Current = y })
                .Where(pair => pair.Previous.ToVersion != pair.Current.ToVersion - 1)
                .Select(pair => pair.Current)
                .Select(method => new VerificationResult(method, VerificationResultEnum.IsNotConsecutive, null));
            foreach (var result in nonConsecutive)
            {
                yield return result;
            }

            var withPreviousReturnType = migrationMethods
                .Select(x => new {MigrationMethod = x, x.ReturnType})
                .ToList();

            var invalidMigrationMethods = new[] { new { MigrationMethod = (MigrationMethod) null, ReturnType = (SimpleType) null } }
                .Concat(withPreviousReturnType)
                .Zip(withPreviousReturnType, (x, y) => new {Previous = x, Current = y})
                .Select(x => new VerificationResult(
                    x.Current.MigrationMethod,
                    VerifyMigrationMethodSignature(x.Current.MigrationMethod, x.Previous.ReturnType),
                    x.Previous.ReturnType)
                );

            foreach (var result in invalidMigrationMethods)
            {
                yield return result;
            }
        }

which the first thing it does is check if the first method is version 1. However if the current version of the data is greater than zero this check will fail as the migration methods that are less than the current are skipped.

HOW DID THIS EVER WORK

NullReferenceException MigrationHashHelper

  1. Change Migratable class
  2. Get Analyzer error
  3. I want fix Analyzer error by CTRL+. I get this error:
System.NullReferenceException : Object reference not set to an instance of an object.
   at Weingartner.Json.Migration.Roslyn.MigrationHashHelper.<>c.<GetMigrationMethods>b__7_0(IMethodSymbol m)
   at System.Linq.Enumerable.WhereSelectEnumerableIterator`2.MoveNext()
   at System.Linq.Enumerable.WhereEnumerableIterator`1.MoveNext()
   at System.Linq.Buffer`1..ctor(IEnumerable`1 source)
   at System.Linq.OrderedEnumerable`1.<GetEnumerator>d__1.MoveNext()
   at System.Collections.Generic.List`1..ctor(IEnumerable`1 collection)
   at System.Linq.Enumerable.ToList[TSource](IEnumerable`1 source)
   at Weingartner.Json.Migration.Roslyn.MigrationHashHelper.GetMigrationMethods(ITypeSymbol typeSymbol)
   at async Weingartner.Json.Migration.Roslyn.AddMigrationMethodCodeFixProvider.AddMigrationMethod(<Unknown Parameters>)
   at async Microsoft.CodeAnalysis.CodeActions.CodeAction.GetChangedSolutionAsync(<Unknown Parameters>)
   at async Microsoft.CodeAnalysis.CodeActions.CodeAction.ComputeOperationsAsync(<Unknown Parameters>)
   at async Microsoft.CodeAnalysis.CodeActions.CodeAction.GetPreviewOperationsAsync(<Unknown Parameters>)
   at async Microsoft.CodeAnalysis.Editor.Implementation.Suggestions.SuggestedAction.GetPreviewResultAsync(<Unknown Parameters>)
   at async Microsoft.CodeAnalysis.Editor.Implementation.Suggestions.SuggestedActionWithNestedFlavors.<>c__DisplayClass11_0.<GetPreviewAsync>b__0(<Unknown Parameters>)
   at async Microsoft.CodeAnalysis.Extensions.IExtensionManagerExtensions.PerformFunctionAsync[T](<Unknown Parameters>)

Correcting migration hash works but I cannot create migration method...

My environment:
Windows 10
Visual Studio 16.6.0
.NET Framework 4.8
Migrations.Json.Net 2.1.2

Weingartner.Json.Migration.Roslyn.MigrationHashAnalyzer cannot be created

Hello, I use Weingartner.Json.Migration.Analyzer 2.1.2 nuget and analyzers do not works. They do not show error when I change code in Migratable class.
Compiler output:

1>CSC : warning CS8032: An instance of analyzer Weingartner.Json.Migration.Roslyn.MigrationHashAnalyzer cannot be created from C:\Users\xx\.nuget\packages\weingartner.json.migration.analyzer\2.1.2\analyzers\dotnet\cs\Weingartner.Json.Migration.Roslyn.dll : Could not load file or assembly 'Microsoft.CodeAnalysis, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35' or one of its dependencies. File not found..
1>CSC : warning CS8032: An instance of analyzer Weingartner.Json.Migration.Roslyn.DataContractAnalyzer cannot be created from C:\Users\xx\.nuget\packages\weingartner.json.migration.analyzer\2.1.2\analyzers\dotnet\cs\Weingartner.Json.Migration.Roslyn.dll : Could not load file or assembly 'Microsoft.CodeAnalysis, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35' or one of its dependencies. File not found..
1>CSC : warning CS8032: An instance of analyzer Weingartner.Json.Migration.Roslyn.MigrationMethodAnalyzer cannot be created from C:\Users\xx\.nuget\packages\weingartner.json.migration.analyzer\2.1.2\analyzers\dotnet\cs\Weingartner.Json.Migration.Roslyn.dll : Could not load file or assembly 'Microsoft.CodeAnalysis, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35' or one of its dependencies. File not found..

Manualy adding Microsoft.CodeAnalysis nuget to project do not solve problem.

NuGet dependencies don't work

When installing Weingartner.Json.Migration into a project, NuGet doesn't add Weingartner.Json.Migration.Analyzer automatically? Maybe we have to specify the .NET framework for the analyzer?

Documentation

Any chance of getting some documentation for this library?

Seems like an awesome way of handling JSON migration, but the barrier to entry is very high without any explanation of how. Reading the tests or the production code example takes a lot of time, especially since there are no explanatory comments.

For example, what does the Migratable attribute do? I am guessing it's in case the class gets renamed, so that the type hash is a persistent identifier that stays the same in case of renaming, but then why do a lot of test cases have an empty string as parameter? Is it simply because they are not testing the renaming support? I'm guessing in production code, one should absolutely never put an empty string in there?

That's just an example of how much guesswork there is to actually use this library. A small documentation that explains the basics would save users a lot of time.

Anyway, thanks a lot for sharing this gem!

Support VS2015 and .Net 4.6 on build server

I've had to upgrade the Migrations.Json.Net package to 4.6. I think that will solve my build problems. However it doesn't build on the build server. I'm not sure what I have to do to get it to work.

Doesn't work with IL2Cpp

Probably because of incomplete reflection support in il2cpp, GetTypeInfo().DefinedMethods doesn't return static methods, instance methods are included. This sadly makes this library unavailable with Unity for now.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.