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); }