Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added structural types and properties documentation properties #2236

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

namespace Microsoft.AspNet.OData.Builder
{
/// <summary>
/// <summary>
/// Represents an <see cref="Attribute"/> 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.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

namespace Microsoft.AspNet.OData.Builder.Conventions
{
/// <summary>
/// <summary>
/// The ActionLinkGenerationConvention calls action.HasActionLink(..) if the action binds to a single entity and has not previously been configured.
/// </summary>
internal class ActionLinkGenerationConvention : IOperationConvention
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System;

namespace Microsoft.AspNet.OData.Builder.Conventions.Attributes
{
/// <summary>
/// Represents an <see cref="Attribute"/> that can be placed on a property or class to document its purpose. The content will be includes in the Odata metadata document.
/// </summary>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public sealed class DescriptionAttribute : Attribute
{
/// <summary>
/// Gets or summary about the purpose of the property or class.
/// </summary>
public string Description { get; }

/// <summary>
/// Gets or sets a detailed description about the purpose of the property or class.
/// </summary>
public string LongDescription { get; set; }

/// <summary>
/// Initializes a new instance of the <see cref="DescriptionAttribute"/> class.
/// </summary>
/// <param name="description">A summary about the purpose of the property or class.</param>
public DescriptionAttribute(string description)
{
Description = description;
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
}
}
}
37 changes: 37 additions & 0 deletions src/Microsoft.AspNet.OData.Shared/Builder/EdmModelHelperMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,46 @@ public static IEdmModel BuildEdmModel(ODataModelBuilder builder)
// build the map from IEdmEntityType to IEdmFunctionImport
model.SetAnnotationValue<BindableOperationFinder>(model, new BindableOperationFinder(model));

// Add annotations for documentation
AddDescriptionAnnotations(builder, model, edmTypeMap);

return model;
}

private static void AddDescriptionAnnotations(ODataModelBuilder builder, EdmModel model, IReadOnlyDictionary<Type, IEdmType> 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}))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand this correctly, I think that maybe ToDictionary is more appropriate than Join. Wouldn't this be equivalent functionality, but a bit more optimized?

var structuredTypeProperties = structuredType.DeclaredProperties.ToDictionary(p => p.Name, p => p);
foreach (var structuralTypeProperty in structuralType.Properties)
{
  if (!structuredTypeProperties .TryGetValue(structuralTypeProperty.Name, out var structuredTypeProperty)
  {
    continue;
  }

  if (!string.IsNullOrEmpty(structuralTypeProperty.Description)
  {
    model.SetDescriptionAnnotation(structuredTypeProperty, structuralTypeProperty.Description);
  }

  if (!string.IsNullOrEmpty(structuralTypeProperty.LongDescription)
  {
    model.SetDescriptionAnnotation(structuredTypeProperty, structuralTypeProperty.LongDescription);
  }
}

{
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<Type, IEdmType> types)
{
Contract.Assert(model != null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ namespace Microsoft.AspNet.OData.Builder
public abstract class PropertyConfiguration
{
private string _name;
private string _description;
private string _longDescription;

/// <summary>
/// Initializes a new instance of the <see cref="PropertyConfiguration"/> class.
Expand All @@ -40,7 +42,7 @@ protected PropertyConfiguration(PropertyInfo property, StructuralTypeConfigurati
QueryConfiguration = new QueryConfiguration();
DerivedTypeConstraints = new DerivedTypeConstraintConfiguration();
}

/// <summary>
/// Gets or sets the name of the property.
/// </summary>
Expand All @@ -59,6 +61,24 @@ public string Name

_name = value;
}
}

/// <summary>
/// Gets or sets the summary for the property.
/// </summary>
public string Description
{
get => _description;
set => _description = value ?? throw Error.PropertyNull();
}

/// <summary>
/// Gets or sets the detailed description of the property.
/// </summary>
public string LongDescription
{
get => _longDescription;
set => _longDescription = value ?? throw Error.PropertyNull();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that throwing is appropriate here because _longDescription will be null by default, so we aren't really protecting against anything

}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ public abstract class StructuralTypeConfiguration : IEdmTypeConfiguration
private PropertyInfo _dynamicPropertyDictionary;
private StructuralTypeConfiguration _baseType;
private bool _baseTypeConfigured;
private string _description;
private string _longDescription;

/// <summary>
/// Initializes a new instance of the <see cref="StructuralTypeConfiguration"/> class.
Expand Down Expand Up @@ -85,6 +87,24 @@ public virtual string FullName
}
}

/// <summary>
/// Gets or sets the summary for the property.
/// </summary>
public string Description
{
get => _description;
set => _description = value ?? throw Error.PropertyNull();
}

/// <summary>
/// Gets or sets the detailed description of the property.
/// </summary>
public string LongDescription
{
get => _longDescription;
set => _longDescription = value ?? throw Error.PropertyNull();
}

/// <summary>
/// Gets or sets the namespace of this EDM type.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -653,6 +653,35 @@ public StructuralTypeConfiguration<TStructuralType> Count(QueryOptionSetting set
return this;
}

/// <summary>
/// Sets the description for this type.
/// </summary>
/// <param name="summary">A short description for this type.</param>
/// <param name="detailedDescription">A detailed description for this type.</param>
/// <returns></returns>
public StructuralTypeConfiguration<TStructuralType> HasDescription(string summary, string detailedDescription)
{
_configuration.Description = summary;
if (!string.IsNullOrEmpty(detailedDescription))
{
_configuration.LongDescription = detailedDescription;
}

return this;
}

/// <summary>
/// Sets the description for this type.
/// </summary>
/// <param name="summary">A short description for this type.</param>
/// <returns></returns>
public StructuralTypeConfiguration<TStructuralType> HasDescription(string summary)
{
_configuration.Description = summary;

return this;
}

/// <summary>
/// Sets sortable properties depends on <see cref="QueryOptionSetting"/> of this structural type.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@
<Compile Include="$(MSBuildThisFileDirectory)Builder\Conventions\Attributes\DerivedTypeConstraintAttributeEdmPropertyConvention.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Builder\Conventions\Attributes\MaxLengthAttributeEdmPropertyConvention.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Builder\Conventions\Attributes\DefaultValueAttributeEdmPropertyConvention.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Builder\Conventions\DescriptionAnnotationConvention.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Builder\DecimalPropertyConfiguration.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Builder\DerivedTypeConstraintAttribute.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Builder\DerivedTypeConstraintConfiguration.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Builder\Conventions\Attributes\DescriptionAttribute.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Builder\LengthPropertyConfiguration.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Builder\PrecisionPropertyConfiguration.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ClrEnumMemberAnnotation.cs" />
Expand Down Expand Up @@ -169,7 +171,7 @@
<Compile Include="$(MSBuildThisFileDirectory)Builder\DynamicPropertyDictionaryAnnotation.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Query\Expressions\AggregationBinder.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Query\Expressions\ComputeBinder.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Query\Expressions\TransformationBinderBase.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Query\Expressions\TransformationBinderBase.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Query\ApplyQueryOption.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Query\Expressions\DynamicTypeWrapper.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Query\Expressions\ExpressionBinderBase.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<IdentityUser>("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<IEdmEntityType>().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<EdmStringConstant>(annotation.Value);
Assert.Equal("Summary", value.Value);
annotation = entityType.VocabularyAnnotations(edmModel).First(e => e.Term.Name == "LongDescription");
value = Assert.IsType<EdmStringConstant>(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<IdentityUser>("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<IEdmEntityType>().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<DescriptionEntity>("DescriptionEntities");
var edmModel = builder.GetEdmModel();

// Assert
Assert.NotNull(edmModel);
var entityType = edmModel.SchemaElements.OfType<IEdmEntityType>().First();
AssertAnnotations(entityType, edmModel);
}

[Fact]
public void ConventionModelBuilder_Description_Set_On_Property_Via_Description_Attribute()
{
// Arrange
var builder = ODataConventionModelBuilderFactory.Create();

// Act
builder.EntitySet<DescriptionPropertyEntity>("IdentityUsers");
var edmModel = builder.GetEdmModel();

// Assert
Assert.NotNull(edmModel);
var entityType = edmModel.SchemaElements.OfType<IEdmEntityType>().First();
var property = entityType.Properties().First(p => p.Name == "Id");

AssertAnnotations(property, edmModel);
}

[Fact]
public void ConventionModelBuild_Work_With_AutoExpandEdmTypeAttribute()
Expand Down Expand Up @@ -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; }
}
}
Loading