private static Expression TryToGetValueWriterForType(Type type, Expression value, ParameterExpression writerParameter, ValueWriterRetriever valueWriterRetriever)
        {
            // Try to get a BinarySerialisationWriter method to call to serialise the value
            // - If it's a Nullable then unwrap the underlying type and try to find a method for that and then we'll have to include some null-checking to the member setter
            //   if we identify a method that will write out the underlying type
            Expression individualMemberSetterForNonNullValue;
            var        nullableTypeInner = GetUnderlyingNullableTypeIfApplicable(type);

            if ((nullableTypeInner ?? type).IsEnum)
            {
                // If the type is an enum then we'll write the underlying type (note: There is some duplication of logic between here and the Serialiser class in the write-
                // underlying-value approach)
                var underlyingType = (nullableTypeInner ?? type).GetEnumUnderlyingType();
                var valueToWrite   = value;
                if (nullableTypeInner != null)
                {
                    // When writing a non-null value from a Nullable<> then we need to write the underlying value - Nullable<T> gets magic treatment from the compiler and we
                    // want to achieve the same sort of thing, which means that (in cases where the Nullable<T> is not null) we write the value AS THE UNDERLYING TYPE into
                    // the field (we DON'T write the value AS NULLABLE<T> into the field). If we're not dealing with a Nullable<T> then we CAN use the value to directly
                    // set the field.
                    valueToWrite = Expression.Convert(valueToWrite, nullableTypeInner);
                }
                valueToWrite = Expression.Convert(valueToWrite, underlyingType);
                individualMemberSetterForNonNullValue = TryToGetValueWriterForType(underlyingType, valueToWrite, writerParameter, valueWriterRetriever);
                if (individualMemberSetterForNonNullValue == null)
                {
                    return(null);
                }
            }
            else
            {
                // If the type is on that we have a dedicated writer method for (ie. it's a primitive or string or DateTime or similar) then use that
                var fieldWriterMethod = TryToGetWriterMethodToSerialiseType(nullableTypeInner ?? type);
                if (fieldWriterMethod != null)
                {
                    // See notes above about Nullable<> handling
                    individualMemberSetterForNonNullValue = Expression.Call(
                        writerParameter,
                        fieldWriterMethod,
                        (nullableTypeInner == null) ? value : Expression.Convert(value, nullableTypeInner)
                        );
                }
                else
                {
                    // Since we weren't able to find a BinarySerialisationWriter method for the value, see if the valueWriterRetriever lookup can provide a member setter that
                    // was generated earlier. If not then we're out of options and have to give up. If we CAN get one then we'll use that (we'll have to bookend it with Object
                    // Start and End calls so that it describes the complete data for a non-reference-tracked instance)
                    var preBuiltMemberSetter = valueWriterRetriever(nullableTypeInner ?? type);
                    if (preBuiltMemberSetter == null)
                    {
                        return(null);
                    }

                    // Instead of calling writer.ObjectStart(type)/ObjectEnd() when writing out the data for complete objects, we'll write out the data in the correct format here
                    // because it will be cheaper as we the ObjectStart method has to determine whether the type has appeared already in the content and, if not, write out the full
                    // type name string and, if it HAS been encountered already, write out just the Name Reference ID - here, we can skip this check and always go straight for the
                    // Name Reference ID approach, which will save us many dictionary lookups if the object graph is large. The reason that we can skip the write-name-or-id check
                    // is that member setters created here will be used in one of two situations:
                    //  1. During a regular serialisation (ie. any ReferenceReuseOptions other than SpeedyButLimited) and only after an instance of the type has been serialised the
                    //     slower way (through the Serialiser class), meaning that the type name has definitely been encountered already
                    //  2. During a SpeedyButLimited serialisation, in which case TypeNamePreLoad data will be emitted at the start of the serialisation data and we can be sure,
                    //     then, that the type name will have been encountered by the reader before it tries to deserialise any instances of the type (because TypeNamePreLoad
                    //     and FieldNamePreLoad data must always be the first content in serialised data)
                    if (preBuiltMemberSetter.SetToDefault)
                    {
                        // This means that we want to record serialisation data that will set this value to default(T)
                        //  - If we're looking at a reference type then that means that we will want to call "writer.Null()" when it comes to writing this value (in this case,
                        //    we can return the expression to do that immediately; if we don't return now then this method will wrap this expression so that it becomes a block
                        //    that says "when serialising, if the source value is null then calling writer.Null() but if the source value is non-null then also call write.Null()"
                        //    and that is a waste of time!)
                        //  - If we're looking at a value type then we need to write the ObjectStart ObjectEnd data so that it writes content for a struct in its default state
                        if (!type.IsValueType)
                        {
                            return(Expression.Call(writerParameter, _writeNullMethod));
                        }

                        var typeNameBytes = GetTypeNameBytes(type);
                        var bytesForEmptyObjectInitialisation = new List <byte> {
                            (byte)BinarySerialisationDataType.ObjectStart
                        };
                        bytesForEmptyObjectInitialisation.AddRange(typeNameBytes.OnlyAsReferenceID);
                        bytesForEmptyObjectInitialisation.Add((byte)BinarySerialisationDataType.ObjectEnd);
                        individualMemberSetterForNonNullValue = Expression.Call(
                            writerParameter,
                            _writeBytesMethod,
                            Expression.Constant(bytesForEmptyObjectInitialisation.ToArray())
                            );
                    }
                    else
                    {
                        var typeNameBytes       = preBuiltMemberSetter.OptionalTypeNameOverideIfNotSettingToDefault ?? GetTypeNameBytes(type);
                        var bytesForObjectStart = new List <byte> {
                            (byte)BinarySerialisationDataType.ObjectStart
                        };
                        bytesForObjectStart.AddRange(typeNameBytes.OnlyAsReferenceID);
                        individualMemberSetterForNonNullValue = Expression.Block(
                            Expression.Call(writerParameter, _writeBytesMethod, Expression.Constant(bytesForObjectStart.ToArray())),
                            Expression.Invoke(
                                preBuiltMemberSetter.MemberSetterIfNotSettingToDefault,
                                (nullableTypeInner == null) ? value : Expression.Convert(value, nullableTypeInner),
                                writerParameter
                                ),
                            Expression.Call(writerParameter, _writeByteMethod, Expression.Constant((byte)BinarySerialisationDataType.ObjectEnd))
                            );
                    }
                }
            }

            // If we're not dealing with a type that may be null then there is nothing more to do!
            if (type.IsValueType && (nullableTypeInner == null))
            {
                return(individualMemberSetterForNonNullValue);
            }

            // If we ARE dealing with a may-be-null value (a Nullable<T> or reference type) then the member-setting expression needs wrapping in an if-null-call-"Null"-
            // method-on-BinarySerialisationWriter check
            // 2018-07-01: The "Null" method on IWrite was added because it's cheaper to write a single Null byte. Otherwise, a null Object would have been written as
            // two for ObjectStart, ObjectEnd; a null Array would have been written as be an ArrayStart byte, the bytes for a null String (for the element type) and
            // an ArrayEnd; a null String would have required three bytes to specify the String data type and then a length of minus one encoded as an Int32_16.
            var ifNullCheck = type.IsValueType
                                ? Expression.Equal(value, Expression.Constant(null, type))           // Null check for structs such as Nullable<T> (this will have the op_Equality or Equals implementations)
                                : Expression.ReferenceEqual(value, Expression.Constant(null, type)); // Null check for reference types, should be quicker to compare it to a null reference than let it implement custom equality logic

            return(Expression.IfThenElse(
                       ifNullCheck,
                       Expression.Call(writerParameter, nameof(BinarySerialisationWriter.Null), typeArguments: Type.EmptyTypes),
                       individualMemberSetterForNonNullValue
                       ));
        }
        /// <summary>
        /// A member setter is a delegate that takes an instance and writes the data for fields and properties to a BinarySerialisationWriter. Note that it will not write the
        /// ObjectStart and ObjectEnd data because the caller should be responsible for tracking references (if the caller is configured to track references), which is tied to
        /// the ObjectStart data. If the caller is tracking references (to either reuse references that appear multiple times in the source data or to identify any circular
        /// references while performing serialisation) then member setters should only be generated for types whose fields and properties are types that do not support reference
        /// reuse, such as primitives and strings. Fields and properties that are primitives or strings or DateTime or TimeSpan or GUIDs (or one-dimensional arrays of any of those
        /// types) can be handled entirely by code within this method but it's also possible to provide member setters for other field or property types via the valueWriterRetriever
        /// argument - again, if the caller is tracking references then it should only pass through member setters via valueWriterRetriever that are for non-reference types, such as
        /// structs) because generating nested member setters in this manner will prevent any reference tracking.
        /// </summary>
        public static MemberSetterGenerationAttemptResult GetMemberSetterAvailability(Type type, IAnalyseTypesForSerialisation typeAnalyser, ValueWriterRetriever valueWriterRetriever)
        {
            if (type == null)
            {
                throw new ArgumentNullException(nameof(type));
            }
            if (typeAnalyser == null)
            {
                throw new ArgumentNullException(nameof(typeAnalyser));
            }
            if (valueWriterRetriever == null)
            {
                throw new ArgumentNullException(nameof(valueWriterRetriever));
            }

            var sourceParameter = Expression.Parameter(type, "source");
            var writerParameter = Expression.Parameter(_writerType, "writer");
            var statements      = new List <Expression>();
            var fieldsSet       = new List <CachedNameData>();

            var(fields, properties) = typeAnalyser.GetFieldsAndProperties(type);
            var membersToConsiderSerialising =
                fields.Select(f =>
            {
                var fieldNameBytes = GetFieldNameBytesIfWantoSerialiseField(f.Member, type);
                if (fieldNameBytes == null)
                {
                    return(null);
                }
                return(new
                {
                    FieldNameBytes = fieldNameBytes,
                    Member = (MemberInfo)f.Member,
                    ValueWriterIfPossibleToGenerate = TryToGetValueWriterForMember(f.Member, f.Member.FieldType, sourceParameter, writerParameter, valueWriterRetriever)
                });
            })
                .Concat(
                    properties.Select(p =>
            {
                var fieldNameBytes = GetFieldNameBytesIfWantoSerialiseProperty(p.Member);
                if (fieldNameBytes == null)
                {
                    return(null);
                }
                return(new
                {
                    FieldNameBytes = fieldNameBytes,
                    Member = (MemberInfo)p.Member,
                    ValueWriterIfPossibleToGenerate = TryToGetValueWriterForMember(p.Member, p.Member.PropertyType, sourceParameter, writerParameter, valueWriterRetriever)
                });
            })
                    )
                .Where(member => member != null);                 // Ignore any fields or properties that we didn't want to serialise

            foreach (var member in membersToConsiderSerialising)
            {
                // If it wasn't possible to get a member setter for this field/property type then it's not possible to generate a member setter for the type that it's on
                if (member.ValueWriterIfPossibleToGenerate == null)
                {
                    return(MemberSetterGenerationAttemptResult.ForFailure(member.Member));
                }

                // Generate the write-FieldName-to-stream method call
                statements.Add(
                    Expression.Call(
                        writerParameter,
                        _writeBytesMethod,
                        Expression.Constant(new[] { (byte)BinarySerialisationDataType.FieldName }.Concat(member.FieldNameBytes.OnlyAsReferenceID).ToArray())
                        )
                    );

                // Write out the value-serialising expression
                statements.Add(member.ValueWriterIfPossibleToGenerate);
                fieldsSet.Add(member.FieldNameBytes);
            }

            // Group all of the field setters together into one call
            Expression body;

            if (statements.Count == 0)
            {
                body = Expression.Empty();
            }
            else if (statements.Count == 1)
            {
                body = statements[0];
            }
            else
            {
                body = Expression.Block(statements);
            }
            return(MemberSetterGenerationAttemptResult.ForSuccess(
                       new MemberSetterDetails(
                           type,
                           Expression.Lambda(
                               body,
                               sourceParameter,
                               writerParameter
                               ),
                           GetTypeNameBytes(type),
                           fieldsSet.ToArray()
                           )
                       ));
        }
        private static Expression TryToGetValueWriterForMember(MemberInfo member, Type memberType, ParameterExpression typedSource, ParameterExpression writerParameter, ValueWriterRetriever valueWriterRetriever)
        {
            if (memberType.IsArray && (memberType.GetArrayRank() == 1))
            {
                // Note: There is some duplication of logic between here and the Serialiser class - namely that we call ArrayStart, then write the elements, then call ArrayEnd
                var elementType = memberType.GetElementType();
                var current     = Expression.Parameter(elementType, "current");
                var valueWriterForFieldElementType = TryToGetValueWriterForType(elementType, current, writerParameter, valueWriterRetriever);
                if (valueWriterForFieldElementType == null)
                {
                    return(null);
                }

                var arrayValue = Expression.MakeMemberAccess(typedSource, member);
                var ifNull     = Expression.Call(
                    writerParameter,
                    nameof(BinarySerialisationWriter.Null),
                    typeArguments: Type.EmptyTypes
                    );
                var arrayLength = Expression.MakeMemberAccess(arrayValue, typeof(Array).GetProperty(nameof(Array.Length)));
                var index       = Expression.Parameter(typeof(int), "i");
                var breakLabel  = Expression.Label("break");
                var ifNotNull   = Expression.Block(
                    Expression.Call(writerParameter, _writeArrayStartMethod, arrayValue, Expression.Constant(elementType)),
                    Expression.Block(
                        new[] { index, current },
                        Expression.Assign(index, Expression.Constant(0)),
                        Expression.Loop(
                            Expression.IfThenElse(
                                Expression.LessThan(index, arrayLength),
                                Expression.Block(
                                    Expression.Assign(current, Expression.ArrayAccess(arrayValue, index)),
                                    valueWriterForFieldElementType,
                                    Expression.Assign(index, Expression.Add(index, Expression.Constant(1)))
                                    ),
                                Expression.Break(breakLabel)
                                ),
                            breakLabel
                            )
                        ),
                    Expression.Call(writerParameter, _writeArrayEndMethod)
                    );
                return(Expression.IfThenElse(
                           Expression.ReferenceEqual(arrayValue, Expression.Constant(null, memberType)),
                           ifNull,
                           ifNotNull
                           ));
            }

            return(TryToGetValueWriterForType(memberType, Expression.MakeMemberAccess(typedSource, member), writerParameter, valueWriterRetriever));
        }