private async Task <Type> CompileResultTypeAsync(DynamicDtoTypeBuildingContext context)
        {
            var proxyClassName = GetProxyTypeName(context.ModelType, "Proxy");

            var tb = GetTypeBuilder(context.ModelType, proxyClassName, new List <Type> {
                typeof(IDynamicDtoProxy)
            });
            var constructor = tb.DefineDefaultConstructor(MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName);

            using (context.OpenNamePrefix(proxyClassName))
            {
                var properties = await GetDynamicPropertiesAsync(context.ModelType, context);

                foreach (var property in properties)
                {
                    if (context.PropertyFilter == null || context.PropertyFilter.Invoke(property.PropertyName))
                    {
                        CreateProperty(tb, property.PropertyName, property.PropertyType);
                    }
                }

                var objectType = tb.CreateType();

                context.ClassCreated(objectType);

                return(objectType);
            }
        }
        private string GetTypeCacheKey(Type type, DynamicDtoTypeBuildingContext context)
        {
            var entityType = DynamicDtoExtensions.GetDynamicDtoEntityType(type);

            if (entityType == null)
            {
                throw new NotSupportedException($"Type '{type.FullName}' is not a dynamic DTO");
            }

            return($"{entityType.FullName}|formFields:{context.AddFormFieldsProperty.ToString().ToLower()}|useEntityDtos:{context.UseDtoForEntityReferences.ToString().ToLower()}");
        }
        private async Task <Type> CompileResultTypeAsync(Type baseType,
                                                         string proxyClassName,
                                                         List <Type> interfaces,
                                                         List <DynamicProperty> properties,
                                                         DynamicDtoTypeBuildingContext context)
        {
            var tb = GetTypeBuilder(baseType, proxyClassName, interfaces.Union(new List <Type> {
                typeof(IDynamicDtoProxy)
            }));
            var constructor = tb.DefineDefaultConstructor(MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName);

            foreach (var property in properties)
            {
                CreateProperty(tb, property.PropertyName, property.PropertyType);
            }

            var objectType = tb.CreateType();

            context.ClassCreated(objectType);

            return(objectType);
        }
        private Type GetEntityReferenceType(EntityPropertyDto propertyDto, DynamicDtoTypeBuildingContext context)
        {
            if (propertyDto.DataType != DataTypes.EntityReference)
            {
                throw new NotSupportedException($"DataType {propertyDto.DataType} is not supported. Expected {DataTypes.EntityReference}");
            }

            if (string.IsNullOrWhiteSpace(propertyDto.EntityType))
            {
                return(null);
            }

            var entityConfig = _entityConfigurationStore.Get(propertyDto.EntityType);

            if (entityConfig == null)
            {
                return(null);
            }

            return(context.UseDtoForEntityReferences
                ? typeof(EntityWithDisplayNameDto <>).MakeGenericType(entityConfig.IdType)
                : entityConfig?.IdType);
        }
        public async Task <Type> BuildDtoFullProxyTypeAsync(Type baseType, DynamicDtoTypeBuildingContext context)
        {
            var cacheKey      = GetTypeCacheKey(baseType, context);
            var cachedDtoItem = await FullProxyCache.GetOrDefaultAsync(cacheKey);

            if (cachedDtoItem != null)
            {
                context.Classes = cachedDtoItem.NestedClasses.ToDictionary(i => i.Key, i => i.Value);
                return(cachedDtoItem.DtoType);
            }

            var proxyClassName = GetProxyTypeName(baseType, "FullProxy");
            var properties     = await GetDynamicPropertiesAsync(baseType, context);

            if (context.AddFormFieldsProperty && !properties.Any(p => p.PropertyName == nameof(IHasFormFieldsList._formFields)))
            {
                properties.Add(new DynamicProperty {
                    PropertyName = nameof(IHasFormFieldsList._formFields), PropertyType = typeof(List <string>)
                });
            }

            var interfaces = new List <Type>();

            if (context.AddFormFieldsProperty)
            {
                interfaces.Add(typeof(IHasFormFieldsList));
            }

            var type = await CompileResultTypeAsync(baseType, proxyClassName, interfaces, properties, context);

            await FullProxyCache.SetAsync(cacheKey, new DynamicDtoProxyCacheItem {
                DtoType       = type,
                NestedClasses = context.Classes.ToDictionary(i => i.Key, i => i.Value)
            });

            return(type);
        }
        public async Task <List <DynamicProperty> > GetDynamicPropertiesAsync(Type type, DynamicDtoTypeBuildingContext context)
        {
            var entityType = DynamicDtoExtensions.GetDynamicDtoEntityType(type);

            if (entityType == null)
            {
                throw new Exception("Failed to extract entity type of the dynamic DTO");
            }

            var properties = new DynamicPropertyList();

            var hardCodedDtoProperties = type.GetProperties().Select(p => p.Name.ToLower()).ToList();

            var configuredProperties = await GetEntityPropertiesAsync(entityType);

            foreach (var property in configuredProperties)
            {
                // skip property if already included into the DTO (hardcoded)
                if (hardCodedDtoProperties.Contains(property.Name.ToLower()))
                {
                    continue;
                }

                if (string.IsNullOrWhiteSpace(property.DataType))
                {
                    Logger.Warn($"Type '{type.FullName}': {nameof(property.DataType)} of property {property.Name} is empty");
                    continue;
                }

                var propertyType = await GetDtoPropertyTypeAsync(property, context);

                if (propertyType != null)
                {
                    properties.Add(property.Name, propertyType);
                }
            }

            // internal fields
            if (context.AddFormFieldsProperty)
            {
                properties.Add(nameof(IHasFormFieldsList._formFields), typeof(List <string>));
            }

            return(properties);
        }
 /// inheritedDoc
 public async Task <Type> BuildDtoProxyTypeAsync(DynamicDtoTypeBuildingContext context)
 {
     return(await CompileResultTypeAsync(context));
 }
        private async Task <Type> BuildNestedTypeAsync(EntityPropertyDto propertyDto, DynamicDtoTypeBuildingContext context)
        {
            if (propertyDto.DataType != DataTypes.Object)
            {
                throw new NotSupportedException($"{nameof(BuildNestedTypeAsync)}: unsupported type of property (expected '{DataTypes.Object}', actual: '{propertyDto.DataType}')");
            }

            // todo: build name of the class according ot the level of the property
            using (context.OpenNamePrefix(propertyDto.Name))
            {
                var className = context.CurrentPrefix.Replace('.', '_');

                var tb = GetTypeBuilder(typeof(object), className, new List <Type> {
                    typeof(IDynamicNestedObject)
                });
                var constructor = tb.DefineDefaultConstructor(MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName);

                foreach (var property in propertyDto.Properties)
                {
                    //if (propertyFilter == null || propertyFilter.Invoke(property.PropertyName))
                    var propertyType = await GetDtoPropertyTypeAsync(property, context);

                    CreateProperty(tb, property.Name, propertyType);
                }

                var objectType = tb.CreateType();

                context.ClassCreated(objectType);

                return(objectType);
            }
        }
        /// <summary>
        /// Returns .Net type that is used to store data for the specified DTO property (according to the property settings)
        /// </summary>
        public async Task <Type> GetDtoPropertyTypeAsync(EntityPropertyDto propertyDto, DynamicDtoTypeBuildingContext context)
        {
            var dataType   = propertyDto.DataType;
            var dataFormat = propertyDto.DataFormat;

            switch (dataType)
            {
            case DataTypes.Guid:
                return(typeof(Guid?));

            case DataTypes.String:
                return(typeof(string));

            case DataTypes.Date:
            case DataTypes.DateTime:
                return(typeof(DateTime?));

            case DataTypes.Time:
                return(typeof(TimeSpan?));

            case DataTypes.Boolean:
                return(typeof(bool?));

            case DataTypes.ReferenceListItem:
                // todo: find a way to check an entity property
                // if it's declared as an enum - get base type of this enum
                // if it's declared as int/Int64 - use this type
                return(typeof(Int64?));

            case DataTypes.Number:
            {
                switch (dataFormat)
                {
                case NumberFormats.Int32:
                    return(typeof(int?));

                case NumberFormats.Int64:
                    return(typeof(Int64?));

                case NumberFormats.Float:
                    return(typeof(float?));

                case NumberFormats.Double:
                    return(typeof(decimal?));

                default:
                    return(typeof(decimal?));
                }
            }

            case DataTypes.EntityReference:
                return(GetEntityReferenceType(propertyDto, context));

            case DataTypes.Array:
            {
                if (propertyDto.ItemsType == null)
                {
                    return(null);
                }

                var nestedType = await GetDtoPropertyTypeAsync(propertyDto.ItemsType, context);

                var arrayType = typeof(List <>).MakeGenericType(nestedType);
                return(arrayType);
            }

            case DataTypes.Object:
                return(await BuildNestedTypeAsync(propertyDto, context));    // JSON content

            default:
                throw new NotSupportedException($"Data type not supported: {dataType}");
            }
        }
        /// <inheritdoc />
        public async Task BindModelAsync(ModelBindingContext bindingContext)
        {
            if (bindingContext == null)
            {
                throw new ArgumentNullException(nameof(bindingContext));
            }

            _logger.AttemptingToBindModel(bindingContext);

            // Special logic for body, treat the model name as string.Empty for the top level
            // object, but allow an override via BinderModelName. The purpose of this is to try
            // and be similar to the behavior for POCOs bound via traditional model binding.
            string modelBindingKey;

            if (bindingContext.IsTopLevelObject)
            {
                modelBindingKey = bindingContext.BinderModelName ?? string.Empty;
            }
            else
            {
                modelBindingKey = bindingContext.ModelName;
            }

            var httpContext = bindingContext.HttpContext;

            httpContext.Request.EnableBuffering(); // enable buffering to read body twice

            #region

            // check if type is already proxied
            var modelType = bindingContext.ModelType;
            var metadata  = bindingContext.ModelMetadata;

            if (modelType is IDynamicDtoProxy)
            {
                throw new NotSupportedException($"{this.GetType().FullName} doesn't support binding of the dynamic poxies. Type `{modelType.FullName}` is implementing `{nameof(IDynamicDtoProxy)}` interface");
            }


            var defaultMetadata = bindingContext.ModelMetadata as DefaultModelMetadata;
            var bindingSettings = defaultMetadata != null
                ? defaultMetadata.Attributes.ParameterAttributes?.OfType <IDynamicMappingSettings>().FirstOrDefault()
                : null;

            bindingSettings = bindingSettings ?? new DynamicMappingSettings();

            var fullDtoBuildContext = new DynamicDtoTypeBuildingContext
            {
                ModelType                 = bindingContext.ModelType,
                PropertyFilter            = propName => true,
                AddFormFieldsProperty     = true,
                UseDtoForEntityReferences = bindingSettings.UseDtoForEntityReferences,
            };
            modelType = await _dtoBuilder.BuildDtoFullProxyTypeAsync(bindingContext.ModelType, fullDtoBuildContext);

            metadata = bindingContext.ModelMetadata.GetMetadataForType(modelType);

            #endregion

            var formatterContext = new InputFormatterContext(
                httpContext,
                modelBindingKey,
                bindingContext.ModelState,
                metadata,
                _readerFactory,
                AllowEmptyBody);

            var formatter = (IInputFormatter?)null;
            for (var i = 0; i < _formatters.Count; i++)
            {
                if (_formatters[i].CanRead(formatterContext))
                {
                    formatter = _formatters[i];
                    _logger.InputFormatterSelected(formatter, formatterContext);
                    break;
                }
                else
                {
                    _logger.InputFormatterRejected(_formatters[i], formatterContext);
                }
            }

            if (formatter == null)
            {
                if (AllowEmptyBody)
                {
                    var hasBody = httpContext.Features.Get <IHttpRequestBodyDetectionFeature>()?.CanHaveBody;
                    hasBody ??= httpContext.Request.ContentLength is not null && httpContext.Request.ContentLength == 0;
                    if (hasBody == false)
                    {
                        bindingContext.Result = ModelBindingResult.Success(model: null);
                        return;
                    }
                }

                _logger.NoInputFormatterSelected(formatterContext);

                var message   = $"Unsupported content type '{httpContext.Request.ContentType}'.";
                var exception = new UnsupportedContentTypeException(message);
                bindingContext.ModelState.AddModelError(modelBindingKey, exception, bindingContext.ModelMetadata);
                _logger.DoneAttemptingToBindModel(bindingContext);
                return;
            }

            try
            {
                var body = await GetRequestBodyAsync(httpContext); // read body (will be used on later stage)

                var result = await formatter.ReadAsync(formatterContext);

                if (result.HasError)
                {
                    // Formatter encountered an error. Do not use the model it returned.
                    _logger.DoneAttemptingToBindModel(bindingContext);
                    return;
                }

                if (result.IsModelSet)
                {
                    if (result.Model is IHasFormFieldsList modelWithFormFields)
                    {
                        // if _formFields is missing in the request - build it automatically according to the json request
                        if (modelWithFormFields._formFields == null)
                        {
                            modelWithFormFields._formFields = GetFormFieldsFromBody(body);
                        }

                        var bindKeys = GetAllPropertyKeys(modelWithFormFields._formFields);

                        var buildContext = new DynamicDtoTypeBuildingContext {
                            ModelType      = bindingContext.ModelType,
                            PropertyFilter = propName => {
                                return(bindKeys.Contains(propName.ToLower()));
                            },
                            AddFormFieldsProperty = true,
                        };
                        var effectiveModelType = await _dtoBuilder.BuildDtoProxyTypeAsync(buildContext);

                        var mapper         = GetMapper(result.Model.GetType(), effectiveModelType, fullDtoBuildContext.Classes);
                        var effectiveModel = mapper.Map(result.Model, result.Model.GetType(), effectiveModelType);

                        bindingContext.Result = ModelBindingResult.Success(effectiveModel);
                    }
                    else
                    {
                        bindingContext.Result = ModelBindingResult.Success(result.Model);
                    }
                }
                else
                {
                    // If the input formatter gives a "no value" result, that's always a model state error,
                    // because BodyModelBinder implicitly regards input as being required for model binding.
                    // If instead the input formatter wants to treat the input as optional, it must do so by
                    // returning InputFormatterResult.Success(defaultForModelType), because input formatters
                    // are responsible for choosing a default value for the model type.
                    var message = bindingContext
                                  .ModelMetadata
                                  .ModelBindingMessageProvider
                                  .MissingRequestBodyRequiredValueAccessor();
                    bindingContext.ModelState.AddModelError(modelBindingKey, message);
                }
            }
            catch (Exception exception) when(exception is InputFormatterException || ShouldHandleException(formatter))
            {
                bindingContext.ModelState.AddModelError(modelBindingKey, exception, bindingContext.ModelMetadata);
            }

            _logger.DoneAttemptingToBindModel(bindingContext);
        }