Add IsRequired and DefaultValue to ApiParameterDescription
This commit is contained in:
parent
17d2545b55
commit
6911e192e4
|
|
@ -41,5 +41,23 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
|||
/// Gets or sets the parameter descriptor.
|
||||
/// </summary>
|
||||
public ParameterDescriptor ParameterDescriptor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value that determines if the parameter is required.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A parameter is considered required if
|
||||
/// <list type="bullet">
|
||||
/// <item>it's bound from the request body (<see cref="BindingSource.Body"/>).</item>
|
||||
/// <item>it's a required route value.</item>
|
||||
/// <item>it has annotations (e.g. BindRequiredAttribute) that indicate it's required.</item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public bool IsRequired { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the default value for a parameter.
|
||||
/// </summary>
|
||||
public object DefaultValue { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.AspNetCore.Routing.Template;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
||||
{
|
||||
internal class ApiParameterContext
|
||||
{
|
||||
public ApiParameterContext(
|
||||
IModelMetadataProvider metadataProvider,
|
||||
ControllerActionDescriptor actionDescriptor,
|
||||
IReadOnlyList<TemplatePart> routeParameters)
|
||||
{
|
||||
MetadataProvider = metadataProvider;
|
||||
ActionDescriptor = actionDescriptor;
|
||||
RouteParameters = routeParameters;
|
||||
|
||||
Results = new List<ApiParameterDescription>();
|
||||
}
|
||||
|
||||
public ControllerActionDescriptor ActionDescriptor { get; }
|
||||
|
||||
public IModelMetadataProvider MetadataProvider { get; }
|
||||
|
||||
public IList<ApiParameterDescription> Results { get; }
|
||||
|
||||
public IReadOnlyList<TemplatePart> RouteParameters { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -4,8 +4,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||
|
|
@ -177,7 +175,7 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
|||
{
|
||||
var visitor = new PseudoModelBindingVisitor(context, actionParameter);
|
||||
|
||||
ModelMetadata metadata = null;
|
||||
ModelMetadata metadata;
|
||||
if (_mvcOptions.AllowValidatingTopLevelNodes &&
|
||||
actionParameter is ControllerParameterDescriptor controllerParameterDescriptor &&
|
||||
_modelMetadataProvider is ModelMetadataProvider provider)
|
||||
|
|
@ -232,6 +230,21 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
|||
}
|
||||
|
||||
// Next, we want to join up any route parameters with those discovered from the action's parameters.
|
||||
// This will result us in creating a parameter representation for each route parameter that does not
|
||||
// have a mapping parameter or bound property.
|
||||
ProcessRouteParameters(context);
|
||||
|
||||
// Set IsRequired=true
|
||||
ProcessIsRequired(context);
|
||||
|
||||
// Set DefaultValue
|
||||
ProcessParameterDefaultValue(context);
|
||||
|
||||
return context.Results;
|
||||
}
|
||||
|
||||
private void ProcessRouteParameters(ApiParameterContext context)
|
||||
{
|
||||
var routeParameters = new Dictionary<string, ApiParameterRouteInfo>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var routeParameter in context.RouteParameters)
|
||||
{
|
||||
|
|
@ -271,8 +284,46 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
|||
Source = BindingSource.Path,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return context.Results;
|
||||
internal static void ProcessIsRequired(ApiParameterContext context)
|
||||
{
|
||||
foreach (var parameter in context.Results)
|
||||
{
|
||||
if (parameter.Source == BindingSource.Body)
|
||||
{
|
||||
parameter.IsRequired = true;
|
||||
}
|
||||
|
||||
if (parameter.ModelMetadata != null && parameter.ModelMetadata.IsBindingRequired)
|
||||
{
|
||||
parameter.IsRequired = true;
|
||||
}
|
||||
|
||||
if (parameter.Source == BindingSource.Path && parameter.RouteInfo != null && !parameter.RouteInfo.IsOptional)
|
||||
{
|
||||
parameter.IsRequired = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static void ProcessParameterDefaultValue(ApiParameterContext context)
|
||||
{
|
||||
foreach (var parameter in context.Results)
|
||||
{
|
||||
if (parameter.Source == BindingSource.Path)
|
||||
{
|
||||
parameter.DefaultValue = parameter.RouteInfo?.DefaultValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (parameter.ParameterDescriptor is ControllerParameterDescriptor controllerParameter &&
|
||||
ParameterDefaultValues.TryGetDeclaredParameterDefaultValue(controllerParameter.ParameterInfo, out var defaultValue))
|
||||
{
|
||||
parameter.DefaultValue = defaultValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ApiParameterRouteInfo CreateRouteInfo(TemplatePart routeParameter)
|
||||
|
|
@ -416,29 +467,6 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
|||
.ToArray();
|
||||
}
|
||||
|
||||
private class ApiParameterContext
|
||||
{
|
||||
public ApiParameterContext(
|
||||
IModelMetadataProvider metadataProvider,
|
||||
ControllerActionDescriptor actionDescriptor,
|
||||
IReadOnlyList<TemplatePart> routeParameters)
|
||||
{
|
||||
MetadataProvider = metadataProvider;
|
||||
ActionDescriptor = actionDescriptor;
|
||||
RouteParameters = routeParameters;
|
||||
|
||||
Results = new List<ApiParameterDescription>();
|
||||
}
|
||||
|
||||
public ControllerActionDescriptor ActionDescriptor { get; }
|
||||
|
||||
public IModelMetadataProvider MetadataProvider { get; }
|
||||
|
||||
public IList<ApiParameterDescription> Results { get; }
|
||||
|
||||
public IReadOnlyList<TemplatePart> RouteParameters { get; }
|
||||
}
|
||||
|
||||
private class ApiParameterDescriptionContext
|
||||
{
|
||||
public ModelMetadata ModelMetadata { get; set; }
|
||||
|
|
|
|||
|
|
@ -30,17 +30,30 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
|
||||
private static object GetParameterDefaultValue(ParameterInfo parameterInfo)
|
||||
{
|
||||
if (!ParameterDefaultValue.TryGetDefaultValue(parameterInfo, out var defaultValue))
|
||||
TryGetDeclaredParameterDefaultValue(parameterInfo, out var defaultValue);
|
||||
if (defaultValue == null && parameterInfo.ParameterType.IsValueType)
|
||||
{
|
||||
var defaultValueAttribute = parameterInfo.GetCustomAttribute<DefaultValueAttribute>(inherit: false);
|
||||
defaultValue = defaultValueAttribute?.Value;
|
||||
|
||||
if (defaultValue == null && parameterInfo.ParameterType.IsValueType)
|
||||
{
|
||||
defaultValue = Activator.CreateInstance(parameterInfo.ParameterType);
|
||||
}
|
||||
defaultValue = Activator.CreateInstance(parameterInfo.ParameterType);
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public static bool TryGetDeclaredParameterDefaultValue(ParameterInfo parameterInfo, out object defaultValue)
|
||||
{
|
||||
if (ParameterDefaultValue.TryGetDefaultValue(parameterInfo, out defaultValue))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var defaultValueAttribute = parameterInfo.GetCustomAttribute<DefaultValueAttribute>(inherit: false);
|
||||
if (defaultValueAttribute != null)
|
||||
{
|
||||
defaultValue = defaultValueAttribute.Value;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
|
|||
using Microsoft.AspNetCore.Mvc.Routing;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.AspNetCore.Routing.Constraints;
|
||||
using Microsoft.AspNetCore.Routing.Template;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using Moq;
|
||||
|
|
@ -1416,6 +1417,172 @@ namespace Microsoft.AspNetCore.Mvc.Description
|
|||
Assert.Equal(typeof(string), comments.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProcessIsRequired_SetsTrue_ForFromBodyParameters()
|
||||
{
|
||||
// Arrange
|
||||
var description = new ApiParameterDescription { Source = BindingSource.Body, };
|
||||
var context = GetApiParameterContext(description);
|
||||
|
||||
// Act
|
||||
DefaultApiDescriptionProvider.ProcessIsRequired(context);
|
||||
|
||||
// Assert
|
||||
Assert.True(description.IsRequired);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProcessIsRequired_SetsTrue_ForParameterDescriptorsWithBindRequired()
|
||||
{
|
||||
// Arrange
|
||||
var description = new ApiParameterDescription
|
||||
{
|
||||
Source = BindingSource.Query,
|
||||
};
|
||||
var context = GetApiParameterContext(description);
|
||||
var modelMetadataProvider = new TestModelMetadataProvider();
|
||||
modelMetadataProvider
|
||||
.ForProperty<Person>(nameof(Person.Name))
|
||||
.BindingDetails(d => d.IsBindingRequired = true);
|
||||
description.ModelMetadata = modelMetadataProvider.GetMetadataForProperty(typeof(Person), nameof(Person.Name));
|
||||
|
||||
// Act
|
||||
DefaultApiDescriptionProvider.ProcessIsRequired(context);
|
||||
|
||||
// Assert
|
||||
Assert.True(description.IsRequired);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProcessIsRequired_SetsTrue_ForRequiredRouteParameterDescriptors()
|
||||
{
|
||||
// Arrange
|
||||
var description = new ApiParameterDescription
|
||||
{
|
||||
Source = BindingSource.Path,
|
||||
RouteInfo = new ApiParameterRouteInfo(),
|
||||
};
|
||||
var context = GetApiParameterContext(description);
|
||||
|
||||
// Act
|
||||
DefaultApiDescriptionProvider.ProcessIsRequired(context);
|
||||
|
||||
// Assert
|
||||
Assert.True(description.IsRequired);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProcessIsRequired_DoesNotSetToTrue_ByDefault()
|
||||
{
|
||||
// Arrange
|
||||
var description = new ApiParameterDescription();
|
||||
var context = GetApiParameterContext(description);
|
||||
|
||||
// Act
|
||||
DefaultApiDescriptionProvider.ProcessIsRequired(context);
|
||||
|
||||
// Assert
|
||||
Assert.False(description.IsRequired);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProcessIsRequired_DoesNotSetToTrue_ForParameterDescriptorsWithValidationRequired()
|
||||
{
|
||||
// Arrange
|
||||
var description = new ApiParameterDescription();
|
||||
var context = GetApiParameterContext(description);
|
||||
var modelMetadataProvider = new TestModelMetadataProvider();
|
||||
modelMetadataProvider
|
||||
.ForProperty<Person>(nameof(Person.Name))
|
||||
.ValidationDetails(d => d.IsRequired = true);
|
||||
description.ModelMetadata = modelMetadataProvider.GetMetadataForProperty(typeof(Person), nameof(Person.Name));
|
||||
|
||||
// Act
|
||||
DefaultApiDescriptionProvider.ProcessIsRequired(context);
|
||||
|
||||
// Assert
|
||||
Assert.False(description.IsRequired);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProcessDefaultValue_SetsDefaultRouteValue()
|
||||
{
|
||||
// Arrange
|
||||
var methodInfo = GetType().GetMethod(nameof(ParameterDefaultValue), BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
var parameterInfo = methodInfo.GetParameters()[0];
|
||||
|
||||
var defaultValue = new object();
|
||||
var description = new ApiParameterDescription
|
||||
{
|
||||
Source = BindingSource.Path,
|
||||
RouteInfo = new ApiParameterRouteInfo { DefaultValue = defaultValue },
|
||||
ParameterDescriptor = new ControllerParameterDescriptor
|
||||
{
|
||||
ParameterInfo = parameterInfo,
|
||||
},
|
||||
};
|
||||
var context = GetApiParameterContext(description);
|
||||
|
||||
// Act
|
||||
DefaultApiDescriptionProvider.ProcessParameterDefaultValue(context);
|
||||
|
||||
// Assert
|
||||
Assert.Same(defaultValue, description.DefaultValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProcessDefaultValue_SetsDefaultValue_FromParameterInfo()
|
||||
{
|
||||
// Arrange
|
||||
var methodInfo = GetType().GetMethod(nameof(ParameterDefaultValue), BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
var parameterInfo = methodInfo.GetParameters()[0];
|
||||
var description = new ApiParameterDescription
|
||||
{
|
||||
Source = BindingSource.Query,
|
||||
ParameterDescriptor = new ControllerParameterDescriptor
|
||||
{
|
||||
ParameterInfo = parameterInfo,
|
||||
},
|
||||
};
|
||||
var context = GetApiParameterContext(description);
|
||||
|
||||
// Act
|
||||
DefaultApiDescriptionProvider.ProcessParameterDefaultValue(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(10, description.DefaultValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProcessDefaultValue_DoesNotSpecifyDefaultValueForValueTypes_WhenNoValueIsSpecified()
|
||||
{
|
||||
// Arrange
|
||||
var methodInfo = GetType().GetMethod(nameof(AcceptsId_Query), BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
var parameterInfo = methodInfo.GetParameters()[0];
|
||||
var description = new ApiParameterDescription
|
||||
{
|
||||
Source = BindingSource.Query,
|
||||
ParameterDescriptor = new ControllerParameterDescriptor
|
||||
{
|
||||
ParameterInfo = parameterInfo,
|
||||
},
|
||||
};
|
||||
var context = GetApiParameterContext(description);
|
||||
|
||||
// Act
|
||||
DefaultApiDescriptionProvider.ProcessParameterDefaultValue(context);
|
||||
|
||||
// Assert
|
||||
Assert.Null(description.DefaultValue);
|
||||
}
|
||||
|
||||
private static ApiParameterContext GetApiParameterContext(ApiParameterDescription description)
|
||||
{
|
||||
var context = new ApiParameterContext(new EmptyModelMetadataProvider(), new ControllerActionDescriptor(), new TemplatePart[0]);
|
||||
context.Results.Add(description);
|
||||
return context;
|
||||
}
|
||||
|
||||
private IReadOnlyList<ApiDescription> GetApiDescriptions(
|
||||
ActionDescriptor action,
|
||||
List<MockInputFormatter> inputFormatters = null,
|
||||
|
|
@ -1711,6 +1878,8 @@ namespace Microsoft.AspNetCore.Mvc.Description
|
|||
{
|
||||
}
|
||||
|
||||
private void ParameterDefaultValue(int value = 10) { }
|
||||
|
||||
private class TestController
|
||||
{
|
||||
[FromRoute]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
|
|
@ -1018,6 +1019,75 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
Assert.Equal(typeof(string).FullName, feedback.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ApiExplorer_Parameters_DefaultValue()
|
||||
{
|
||||
// Arrange & Act
|
||||
var response = await Client.GetAsync("ApiExplorerParameters/DefaultValueParameters");
|
||||
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
var result = JsonConvert.DeserializeObject<List<ApiExplorerData>>(body);
|
||||
|
||||
// Assert
|
||||
var description = Assert.Single(result);
|
||||
var parameters = description.ParameterDescriptions;
|
||||
|
||||
Assert.Collection(
|
||||
parameters,
|
||||
parameter =>
|
||||
{
|
||||
Assert.Equal("searchTerm", parameter.Name);
|
||||
Assert.Null(parameter.DefaultValue);
|
||||
},
|
||||
parameter =>
|
||||
{
|
||||
Assert.Equal("top", parameter.Name);
|
||||
Assert.Equal("10", parameter.DefaultValue);
|
||||
},
|
||||
parameter =>
|
||||
{
|
||||
Assert.Equal("searchDay", parameter.Name);
|
||||
Assert.Equal(nameof(DayOfWeek.Wednesday), parameter.DefaultValue);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ApiExplorer_Parameters_IsRequired()
|
||||
{
|
||||
// Arrange & Act
|
||||
var response = await Client.GetAsync("ApiExplorerParameters/IsRequiredParameters");
|
||||
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
var result = JsonConvert.DeserializeObject<List<ApiExplorerData>>(body);
|
||||
|
||||
// Assert
|
||||
var description = Assert.Single(result);
|
||||
var parameters = description.ParameterDescriptions;
|
||||
|
||||
Assert.Collection(
|
||||
parameters,
|
||||
parameter =>
|
||||
{
|
||||
Assert.Equal("requiredParam", parameter.Name);
|
||||
Assert.True(parameter.IsRequired);
|
||||
},
|
||||
parameter =>
|
||||
{
|
||||
Assert.Equal("notRequiredParam", parameter.Name);
|
||||
Assert.False(parameter.IsRequired);
|
||||
},
|
||||
parameter =>
|
||||
{
|
||||
Assert.Equal("Id", parameter.Name);
|
||||
Assert.True(parameter.IsRequired);
|
||||
},
|
||||
parameter =>
|
||||
{
|
||||
Assert.Equal("Name", parameter.Name);
|
||||
Assert.False(parameter.IsRequired);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ApiExplorer_Updates_WhenActionDescriptorCollectionIsUpdated()
|
||||
{
|
||||
|
|
@ -1111,6 +1181,10 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
public string Source { get; set; }
|
||||
|
||||
public string Type { get; set; }
|
||||
|
||||
public string DefaultValue { get; set; }
|
||||
|
||||
public bool IsRequired { get; set; }
|
||||
}
|
||||
|
||||
// Used to serialize data between client and server
|
||||
|
|
|
|||
|
|
@ -28,8 +28,8 @@ namespace ApiExplorerWebSite
|
|||
|
||||
public void OnResourceExecuting(ResourceExecutingContext context)
|
||||
{
|
||||
var controllerActionDescriptor = context.ActionDescriptor as ControllerActionDescriptor;
|
||||
if (controllerActionDescriptor != null && controllerActionDescriptor.MethodInfo.IsDefined(typeof(PassThruAttribute)))
|
||||
if (context.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor &&
|
||||
controllerActionDescriptor.MethodInfo.IsDefined(typeof(PassThruAttribute)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
|
@ -69,6 +69,8 @@ namespace ApiExplorerWebSite
|
|||
Name = parameter.Name,
|
||||
Source = parameter.Source.Id,
|
||||
Type = parameter.Type?.FullName,
|
||||
DefaultValue = parameter.DefaultValue?.ToString(),
|
||||
IsRequired = parameter.IsRequired,
|
||||
};
|
||||
|
||||
if (parameter.RouteInfo != null)
|
||||
|
|
@ -143,6 +145,10 @@ namespace ApiExplorerWebSite
|
|||
public string Source { get; set; }
|
||||
|
||||
public string Type { get; set; }
|
||||
|
||||
public string DefaultValue { get; set; }
|
||||
|
||||
public bool IsRequired { get; set; }
|
||||
}
|
||||
|
||||
// Used to serialize data between client and server
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
|
||||
namespace ApiExplorerWebSite.Controllers
|
||||
{
|
||||
|
|
@ -28,5 +30,13 @@ namespace ApiExplorerWebSite.Controllers
|
|||
public void ComplexModel([FromQuery] OrderDTO order)
|
||||
{
|
||||
}
|
||||
|
||||
public void DefaultValueParameters(string searchTerm, int top = 10, DayOfWeek searchDay = DayOfWeek.Wednesday)
|
||||
{
|
||||
}
|
||||
|
||||
public void IsRequiredParameters([BindRequired] string requiredParam, string notRequiredParam, Product product)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,17 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
|
||||
namespace ApiExplorerWebSite
|
||||
{
|
||||
public class Product
|
||||
{
|
||||
[BindRequired]
|
||||
public int Id { get; set; }
|
||||
|
||||
[Required]
|
||||
public string Name { get; set; }
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue