diff --git a/src/Microsoft.AspNet.OData.Shared/Builder/ActionOnDeleteAttribute.cs b/src/Microsoft.AspNet.OData.Shared/Builder/ActionOnDeleteAttribute.cs index 7de1e0b302..a71d0aacce 100644 --- a/src/Microsoft.AspNet.OData.Shared/Builder/ActionOnDeleteAttribute.cs +++ b/src/Microsoft.AspNet.OData.Shared/Builder/ActionOnDeleteAttribute.cs @@ -6,7 +6,7 @@ namespace Microsoft.AspNet.OData.Builder { - /// + /// /// Represents an that can be placed on a navigation property to specify the applied /// action whether delete should also remove the associated item on the other end of the association. /// diff --git a/src/Microsoft.AspNet.OData.Shared/Builder/Conventions/ActionLinkGenerationConvention.cs b/src/Microsoft.AspNet.OData.Shared/Builder/Conventions/ActionLinkGenerationConvention.cs index 554b1837fc..59c8891a2d 100644 --- a/src/Microsoft.AspNet.OData.Shared/Builder/Conventions/ActionLinkGenerationConvention.cs +++ b/src/Microsoft.AspNet.OData.Shared/Builder/Conventions/ActionLinkGenerationConvention.cs @@ -5,7 +5,7 @@ namespace Microsoft.AspNet.OData.Builder.Conventions { - /// + /// /// The ActionLinkGenerationConvention calls action.HasActionLink(..) if the action binds to a single entity and has not previously been configured. /// internal class ActionLinkGenerationConvention : IOperationConvention diff --git a/src/Microsoft.AspNet.OData.Shared/Builder/Conventions/Attributes/DescriptionAttribute.cs b/src/Microsoft.AspNet.OData.Shared/Builder/Conventions/Attributes/DescriptionAttribute.cs new file mode 100644 index 0000000000..a454ccf66e --- /dev/null +++ b/src/Microsoft.AspNet.OData.Shared/Builder/Conventions/Attributes/DescriptionAttribute.cs @@ -0,0 +1,30 @@ +using System; + +namespace Microsoft.AspNet.OData.Builder.Conventions.Attributes +{ + /// + /// Represents an that can be placed on a property or class to document its purpose. The content will be includes in the Odata metadata document. + /// + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Class, AllowMultiple = false, Inherited = false)] + public sealed class DescriptionAttribute : Attribute + { + /// + /// Gets or summary about the purpose of the property or class. + /// + public string Description { get; } + + /// + /// Gets or sets a detailed description about the purpose of the property or class. + /// + public string LongDescription { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// A summary about the purpose of the property or class. + public DescriptionAttribute(string description) + { + Description = description; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.OData.Shared/Builder/Conventions/DescriptionAnnotationConvention.cs b/src/Microsoft.AspNet.OData.Shared/Builder/Conventions/DescriptionAnnotationConvention.cs new file mode 100644 index 0000000000..8cb41e9105 --- /dev/null +++ b/src/Microsoft.AspNet.OData.Shared/Builder/Conventions/DescriptionAnnotationConvention.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using Microsoft.AspNet.OData.Builder.Conventions.Attributes; + +namespace Microsoft.AspNet.OData.Builder.Conventions +{ + internal class DescriptionAnnotationConvention : IEdmTypeConvention + { + public void Apply(IEdmTypeConfiguration edmTypeConfiguration, ODataConventionModelBuilder model) + { + if (!(edmTypeConfiguration is StructuralTypeConfiguration structuralType)) return; + + var attribute = (DescriptionAttribute) structuralType.ClrType.GetCustomAttribute(typeof(DescriptionAttribute), true); + if (attribute != null) + { + structuralType.Description = attribute.Description; + if (!string.IsNullOrEmpty(attribute.LongDescription)) + { + structuralType.LongDescription = attribute.LongDescription; + } + } + + foreach (var property in structuralType.Properties) + { + attribute = (DescriptionAttribute) property.PropertyInfo.GetCustomAttribute(typeof(DescriptionAttribute), true); + if (attribute != null) + { + property.Description = attribute.Description; + if (!string.IsNullOrEmpty(attribute.LongDescription)) + { + property.LongDescription = attribute.LongDescription; + } + } + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.OData.Shared/Builder/EdmModelHelperMethods.cs b/src/Microsoft.AspNet.OData.Shared/Builder/EdmModelHelperMethods.cs index 9d850b53a0..7267e217e6 100644 --- a/src/Microsoft.AspNet.OData.Shared/Builder/EdmModelHelperMethods.cs +++ b/src/Microsoft.AspNet.OData.Shared/Builder/EdmModelHelperMethods.cs @@ -66,9 +66,46 @@ public static IEdmModel BuildEdmModel(ODataModelBuilder builder) // build the map from IEdmEntityType to IEdmFunctionImport model.SetAnnotationValue(model, new BindableOperationFinder(model)); + // Add annotations for documentation + AddDescriptionAnnotations(builder, model, edmTypeMap); + return model; } + private static void AddDescriptionAnnotations(ODataModelBuilder builder, EdmModel model, IReadOnlyDictionary edmTypeMap) + { + foreach (var structuralType in builder.StructuralTypes) + { + + if (!edmTypeMap.TryGetValue(structuralType.ClrType, out var edmType)) continue; + if (!(edmType is IEdmVocabularyAnnotatable target)) continue; + + if (!string.IsNullOrEmpty(structuralType.Description)) + { + model.SetDescriptionAnnotation(target, structuralType.Description); + } + if (!string.IsNullOrEmpty(structuralType.LongDescription)) + { + model.SetLongDescriptionAnnotation(target, structuralType.LongDescription); + } + + if (edmType is IEdmStructuredType structuredType) + { + foreach (var entry in structuredType.DeclaredProperties.Join(structuralType.Properties, p => p.Name, p => p.Name, (property, configuration) => new {property, configuration})) + { + if (!string.IsNullOrEmpty(entry.configuration.Description)) + { + model.SetDescriptionAnnotation(entry.property, entry.configuration.Description); + } + if (!string.IsNullOrEmpty(entry.configuration.LongDescription)) + { + model.SetLongDescriptionAnnotation(entry.property, entry.configuration.LongDescription); + } + } + } + } + } + private static void AddTypes(this EdmModel model, Dictionary types) { Contract.Assert(model != null); diff --git a/src/Microsoft.AspNet.OData.Shared/Builder/ODataConventionModelBuilder.cs b/src/Microsoft.AspNet.OData.Shared/Builder/ODataConventionModelBuilder.cs index 177685b581..860b996120 100644 --- a/src/Microsoft.AspNet.OData.Shared/Builder/ODataConventionModelBuilder.cs +++ b/src/Microsoft.AspNet.OData.Shared/Builder/ODataConventionModelBuilder.cs @@ -71,6 +71,9 @@ public partial class ODataConventionModelBuilder : ODataModelBuilder // IEdmFunctionImportConventions's new ActionLinkGenerationConvention(), new FunctionLinkGenerationConvention(), + + // Documentation conventions + new DescriptionAnnotationConvention(), }; // These hashset's keep track of edmtypes/navigation sources for which conventions diff --git a/src/Microsoft.AspNet.OData.Shared/Builder/PropertyConfiguration.cs b/src/Microsoft.AspNet.OData.Shared/Builder/PropertyConfiguration.cs index caf9667fb0..9e3a04a0c5 100644 --- a/src/Microsoft.AspNet.OData.Shared/Builder/PropertyConfiguration.cs +++ b/src/Microsoft.AspNet.OData.Shared/Builder/PropertyConfiguration.cs @@ -15,6 +15,8 @@ namespace Microsoft.AspNet.OData.Builder public abstract class PropertyConfiguration { private string _name; + private string _description; + private string _longDescription; /// /// Initializes a new instance of the class. @@ -40,7 +42,7 @@ protected PropertyConfiguration(PropertyInfo property, StructuralTypeConfigurati QueryConfiguration = new QueryConfiguration(); DerivedTypeConstraints = new DerivedTypeConstraintConfiguration(); } - + /// /// Gets or sets the name of the property. /// @@ -59,6 +61,24 @@ public string Name _name = value; } + } + + /// + /// Gets or sets the summary for the property. + /// + public string Description + { + get => _description; + set => _description = value ?? throw Error.PropertyNull(); + } + + /// + /// Gets or sets the detailed description of the property. + /// + public string LongDescription + { + get => _longDescription; + set => _longDescription = value ?? throw Error.PropertyNull(); } /// diff --git a/src/Microsoft.AspNet.OData.Shared/Builder/StructuralTypeConfiguration.cs b/src/Microsoft.AspNet.OData.Shared/Builder/StructuralTypeConfiguration.cs index a9b6123315..be4dc1eeea 100644 --- a/src/Microsoft.AspNet.OData.Shared/Builder/StructuralTypeConfiguration.cs +++ b/src/Microsoft.AspNet.OData.Shared/Builder/StructuralTypeConfiguration.cs @@ -23,6 +23,8 @@ public abstract class StructuralTypeConfiguration : IEdmTypeConfiguration private PropertyInfo _dynamicPropertyDictionary; private StructuralTypeConfiguration _baseType; private bool _baseTypeConfigured; + private string _description; + private string _longDescription; /// /// Initializes a new instance of the class. @@ -85,6 +87,24 @@ public virtual string FullName } } + /// + /// Gets or sets the summary for the property. + /// + public string Description + { + get => _description; + set => _description = value ?? throw Error.PropertyNull(); + } + + /// + /// Gets or sets the detailed description of the property. + /// + public string LongDescription + { + get => _longDescription; + set => _longDescription = value ?? throw Error.PropertyNull(); + } + /// /// Gets or sets the namespace of this EDM type. /// diff --git a/src/Microsoft.AspNet.OData.Shared/Builder/StructuralTypeConfigurationOfTStructuralType.cs b/src/Microsoft.AspNet.OData.Shared/Builder/StructuralTypeConfigurationOfTStructuralType.cs index 4ad57a656c..95f16fdf76 100644 --- a/src/Microsoft.AspNet.OData.Shared/Builder/StructuralTypeConfigurationOfTStructuralType.cs +++ b/src/Microsoft.AspNet.OData.Shared/Builder/StructuralTypeConfigurationOfTStructuralType.cs @@ -653,6 +653,35 @@ public StructuralTypeConfiguration Count(QueryOptionSetting set return this; } + /// + /// Sets the description for this type. + /// + /// A short description for this type. + /// A detailed description for this type. + /// + public StructuralTypeConfiguration HasDescription(string summary, string detailedDescription) + { + _configuration.Description = summary; + if (!string.IsNullOrEmpty(detailedDescription)) + { + _configuration.LongDescription = detailedDescription; + } + + return this; + } + + /// + /// Sets the description for this type. + /// + /// A short description for this type. + /// + public StructuralTypeConfiguration HasDescription(string summary) + { + _configuration.Description = summary; + + return this; + } + /// /// Sets sortable properties depends on of this structural type. /// diff --git a/src/Microsoft.AspNet.OData.Shared/Microsoft.AspNet.OData.Shared.projitems b/src/Microsoft.AspNet.OData.Shared/Microsoft.AspNet.OData.Shared.projitems index cf51c1570a..72b5db0731 100644 --- a/src/Microsoft.AspNet.OData.Shared/Microsoft.AspNet.OData.Shared.projitems +++ b/src/Microsoft.AspNet.OData.Shared/Microsoft.AspNet.OData.Shared.projitems @@ -14,9 +14,11 @@ + + @@ -169,7 +171,7 @@ - + diff --git a/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/Builder/Conventions/ODataConventionModelBuilderTests.cs b/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/Builder/Conventions/ODataConventionModelBuilderTests.cs index eba31dcc54..b998bfa205 100644 --- a/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/Builder/Conventions/ODataConventionModelBuilderTests.cs +++ b/test/UnitTest/Microsoft.AspNet.OData.Test.Shared/Builder/Conventions/ODataConventionModelBuilderTests.cs @@ -10,6 +10,7 @@ using System.Reflection; using System.Runtime.Serialization; using Microsoft.AspNet.OData.Builder; +using Microsoft.AspNet.OData.Builder.Conventions.Attributes; using Microsoft.AspNet.OData.Formatter; using Microsoft.AspNet.OData.Query; using Microsoft.AspNet.OData.Test.Abstraction; @@ -3134,6 +3135,90 @@ public void ConventionModelBuilder_Work_With_ExpilitPropertyDeclare() Assert.False(entityType.Properties().First(p => p.Name.Equals("UserType")).Type.IsNullable); Assert.False(entityType.Properties().First(p => p.Name.Equals("Contacts")).Type.IsNullable); } + + [Fact] + public void ConventionModelBuilder_Description_Set_On_EntityType() + { + // Arrange + var builder = ODataConventionModelBuilderFactory.Create(); + + // Act + var user = builder.EntitySet("IdentityUsers"); + user.EntityType.HasKey(p => new { p.Provider, p.UserId }); + user.EntityType.HasDescription("Summary", "Detailed description"); + var edmModel = builder.GetEdmModel(); + + // Assert + Assert.NotNull(edmModel); + var entityType = edmModel.SchemaElements.OfType().First(); + AssertAnnotations(entityType, edmModel); + } + + private static void AssertAnnotations(IEdmVocabularyAnnotatable entityType, IEdmModel edmModel) + { + var annotation = entityType.VocabularyAnnotations(edmModel).First(e => e.Term.Name == "Description"); + var value = Assert.IsType(annotation.Value); + Assert.Equal("Summary", value.Value); + annotation = entityType.VocabularyAnnotations(edmModel).First(e => e.Term.Name == "LongDescription"); + value = Assert.IsType(annotation.Value); + Assert.Equal("Detailed description", value.Value); + } + + [Fact] + public void ConventionModelBuilder_Description_Set_On_Property() + { + // Arrange + var builder = ODataConventionModelBuilderFactory.Create(); + + // Act + var user = builder.EntitySet("IdentityUsers"); + user.EntityType.HasKey(p => new { p.Provider, p.UserId }); + var propertyConfiguration = user.EntityType.Property(i => i.Name); + propertyConfiguration.Description = "Summary"; + propertyConfiguration.LongDescription = "Detailed description"; + var edmModel = builder.GetEdmModel(); + + // Assert + Assert.NotNull(edmModel); + var entityType = edmModel.SchemaElements.OfType().First(); + var property = entityType.Properties().First(p => p.Name == "Name"); + + AssertAnnotations(property, edmModel); + } + + [Fact] + public void ConventionModelBuilder_Description_Set_On_Entity_Via_Description_Attribute() + { + // Arrange + var builder = ODataConventionModelBuilderFactory.Create(); + + // Act + builder.EntitySet("DescriptionEntities"); + var edmModel = builder.GetEdmModel(); + + // Assert + Assert.NotNull(edmModel); + var entityType = edmModel.SchemaElements.OfType().First(); + AssertAnnotations(entityType, edmModel); + } + + [Fact] + public void ConventionModelBuilder_Description_Set_On_Property_Via_Description_Attribute() + { + // Arrange + var builder = ODataConventionModelBuilderFactory.Create(); + + // Act + builder.EntitySet("IdentityUsers"); + var edmModel = builder.GetEdmModel(); + + // Assert + Assert.NotNull(edmModel); + var entityType = edmModel.SchemaElements.OfType().First(); + var property = entityType.Properties().First(p => p.Name == "Id"); + + AssertAnnotations(property, edmModel); + } [Fact] public void ConventionModelBuild_Work_With_AutoExpandEdmTypeAttribute() @@ -3720,4 +3805,18 @@ public class MaxLengthEntity public string NonLength { get; set; } } + + [Description("Summary", LongDescription = "Detailed description")] + public class DescriptionEntity + { + [Key] + public int Id { get; set; } + } + + public class DescriptionPropertyEntity + { + [Key] + [Description("Summary", LongDescription = "Detailed description")] + public int Id { get; set; } + } } diff --git a/test/UnitTest/Microsoft.AspNetCore.OData.Test/PublicApi/Microsoft.AspNetCore.OData.PublicApi.bsl b/test/UnitTest/Microsoft.AspNetCore.OData.Test/PublicApi/Microsoft.AspNetCore.OData.PublicApi.bsl index 82c87bfecd..0d87035150 100644 --- a/test/UnitTest/Microsoft.AspNetCore.OData.Test/PublicApi/Microsoft.AspNetCore.OData.PublicApi.bsl +++ b/test/UnitTest/Microsoft.AspNetCore.OData.Test/PublicApi/Microsoft.AspNetCore.OData.PublicApi.bsl @@ -1104,6 +1104,7 @@ public abstract class Microsoft.AspNet.OData.Builder.PropertyConfiguration { bool AutoExpand { public get; public set; } StructuralTypeConfiguration DeclaringType { public get; } DerivedTypeConstraintConfiguration DerivedTypeConstraints { public get; } + string Description { public get; public set; } bool DisableAutoExpandWhenSelectIsPresent { public get; public set; } bool IsRestricted { public get; } PropertyKind Kind { public abstract get; } @@ -1173,6 +1174,7 @@ public abstract class Microsoft.AspNet.OData.Builder.StructuralTypeConfiguration bool BaseTypeConfigured { public virtual get; } StructuralTypeConfiguration BaseTypeInternal { protected virtual get; } System.Type ClrType { public virtual get; } + string Description { public virtual get; public virtual set; } System.Reflection.PropertyInfo DynamicPropertyDictionary { public get; } System.Collections.Generic.IDictionary`2[[System.Reflection.PropertyInfo],[Microsoft.AspNet.OData.Builder.PropertyConfiguration]] ExplicitProperties { protected get; } string FullName { public virtual get; } @@ -1231,6 +1233,7 @@ public abstract class Microsoft.AspNet.OData.Builder.StructuralTypeConfiguration public StructuralTypeConfiguration`1 Filter (QueryOptionSetting setting) public StructuralTypeConfiguration`1 Filter (string[] properties) public StructuralTypeConfiguration`1 Filter (QueryOptionSetting setting, string[] properties) + public StructuralTypeConfiguration`1 HasDescription (string value) public void HasDynamicProperties (Expression`1 propertyExpression) public NavigationPropertyConfiguration HasMany (Expression`1 navigationPropertyExpression) public NavigationPropertyConfiguration HasOptional (Expression`1 navigationPropertyExpression) @@ -1248,7 +1251,6 @@ public abstract class Microsoft.AspNet.OData.Builder.StructuralTypeConfiguration public StructuralTypeConfiguration`1 OrderBy (QueryOptionSetting setting, string[] properties) public StructuralTypeConfiguration`1 Page () public StructuralTypeConfiguration`1 Page (System.Nullable`1[[System.Int32]] maxTopValue, System.Nullable`1[[System.Int32]] pageSizeValue) - public LengthPropertyConfiguration Property (Expression`1 propertyExpression) public DecimalPropertyConfiguration Property (Expression`1 propertyExpression) public PrecisionPropertyConfiguration Property (Expression`1 propertyExpression) public PrecisionPropertyConfiguration Property (Expression`1 propertyExpression) @@ -1257,8 +1259,9 @@ public abstract class Microsoft.AspNet.OData.Builder.StructuralTypeConfiguration public PrecisionPropertyConfiguration Property (Expression`1 propertyExpression) public PrecisionPropertyConfiguration Property (Expression`1 propertyExpression) public DecimalPropertyConfiguration Property (Expression`1 propertyExpression) - public PrecisionPropertyConfiguration Property (Expression`1 propertyExpression) public PrimitivePropertyConfiguration Property (Expression`1 propertyExpression) + public PrecisionPropertyConfiguration Property (Expression`1 propertyExpression) + public LengthPropertyConfiguration Property (Expression`1 propertyExpression) public PrimitivePropertyConfiguration Property (Expression`1 propertyExpression) public PrimitivePropertyConfiguration Property (Expression`1 propertyExpression) public StructuralTypeConfiguration`1 Select () diff --git a/test/UnitTest/Microsoft.AspNetCore.OData.Test/PublicApi/Microsoft.AspNetCore3x.OData.PublicApi.bsl b/test/UnitTest/Microsoft.AspNetCore.OData.Test/PublicApi/Microsoft.AspNetCore3x.OData.PublicApi.bsl index eaa74317bf..fd479162ab 100644 --- a/test/UnitTest/Microsoft.AspNetCore.OData.Test/PublicApi/Microsoft.AspNetCore3x.OData.PublicApi.bsl +++ b/test/UnitTest/Microsoft.AspNetCore.OData.Test/PublicApi/Microsoft.AspNetCore3x.OData.PublicApi.bsl @@ -1104,6 +1104,7 @@ public abstract class Microsoft.AspNet.OData.Builder.PropertyConfiguration { bool AutoExpand { public get; public set; } StructuralTypeConfiguration DeclaringType { public get; } DerivedTypeConstraintConfiguration DerivedTypeConstraints { public get; } + string Description { public get; public set; } bool DisableAutoExpandWhenSelectIsPresent { public get; public set; } bool IsRestricted { public get; } PropertyKind Kind { public abstract get; } @@ -1173,6 +1174,7 @@ public abstract class Microsoft.AspNet.OData.Builder.StructuralTypeConfiguration bool BaseTypeConfigured { public virtual get; } StructuralTypeConfiguration BaseTypeInternal { protected virtual get; } System.Type ClrType { public virtual get; } + string Description { public virtual get; public virtual set; } System.Reflection.PropertyInfo DynamicPropertyDictionary { public get; } System.Collections.Generic.IDictionary`2[[System.Reflection.PropertyInfo],[Microsoft.AspNet.OData.Builder.PropertyConfiguration]] ExplicitProperties { protected get; } string FullName { public virtual get; } @@ -1231,6 +1233,7 @@ public abstract class Microsoft.AspNet.OData.Builder.StructuralTypeConfiguration public StructuralTypeConfiguration`1 Filter (QueryOptionSetting setting) public StructuralTypeConfiguration`1 Filter (string[] properties) public StructuralTypeConfiguration`1 Filter (QueryOptionSetting setting, string[] properties) + public StructuralTypeConfiguration`1 HasDescription (string value) public void HasDynamicProperties (Expression`1 propertyExpression) public NavigationPropertyConfiguration HasMany (Expression`1 navigationPropertyExpression) public NavigationPropertyConfiguration HasOptional (Expression`1 navigationPropertyExpression) @@ -1248,7 +1251,6 @@ public abstract class Microsoft.AspNet.OData.Builder.StructuralTypeConfiguration public StructuralTypeConfiguration`1 OrderBy (QueryOptionSetting setting, string[] properties) public StructuralTypeConfiguration`1 Page () public StructuralTypeConfiguration`1 Page (System.Nullable`1[[System.Int32]] maxTopValue, System.Nullable`1[[System.Int32]] pageSizeValue) - public LengthPropertyConfiguration Property (Expression`1 propertyExpression) public DecimalPropertyConfiguration Property (Expression`1 propertyExpression) public PrecisionPropertyConfiguration Property (Expression`1 propertyExpression) public PrecisionPropertyConfiguration Property (Expression`1 propertyExpression) @@ -1257,8 +1259,9 @@ public abstract class Microsoft.AspNet.OData.Builder.StructuralTypeConfiguration public PrecisionPropertyConfiguration Property (Expression`1 propertyExpression) public PrecisionPropertyConfiguration Property (Expression`1 propertyExpression) public DecimalPropertyConfiguration Property (Expression`1 propertyExpression) - public PrecisionPropertyConfiguration Property (Expression`1 propertyExpression) public PrimitivePropertyConfiguration Property (Expression`1 propertyExpression) + public PrecisionPropertyConfiguration Property (Expression`1 propertyExpression) + public LengthPropertyConfiguration Property (Expression`1 propertyExpression) public PrimitivePropertyConfiguration Property (Expression`1 propertyExpression) public PrimitivePropertyConfiguration Property (Expression`1 propertyExpression) public StructuralTypeConfiguration`1 Select ()