Kann eine C # -Klasse Attribute von ihrer Schnittstelle erben?

114

Dies scheint "nein" zu bedeuten. Welches ist unglücklich.

[AttributeUsage(AttributeTargets.Interface | AttributeTargets.Class,
 AllowMultiple = true, Inherited = true)]
public class CustomDescriptionAttribute : Attribute
{
    public string Description { get; private set; }

    public CustomDescriptionAttribute(string description)
    {
        Description = description;
    }
}

[CustomDescription("IProjectController")]
public interface IProjectController
{
    void Create(string projectName);
}

internal class ProjectController : IProjectController
{
    public void Create(string projectName)
    {
    }
}

[TestFixture]
public class CustomDescriptionAttributeTests
{
    [Test]
    public void ProjectController_ShouldHaveCustomDescriptionAttribute()
    {
        Type type = typeof(ProjectController);
        object[] attributes = type.GetCustomAttributes(
            typeof(CustomDescriptionAttribute),
            true);

        // NUnit.Framework.AssertionException:   Expected: 1   But was:  0
        Assert.AreEqual(1, attributes.Length);
    }
}

Kann eine Klasse Attribute von einer Schnittstelle erben? Oder belle ich hier den falschen Baum an?

Roger Lipscombe
quelle

Antworten:

73

Nein. Wenn Sie eine Schnittstelle implementieren oder Mitglieder in einer abgeleiteten Klasse überschreiben, müssen Sie die Attribute erneut deklarieren.

Wenn Sie sich nur für ComponentModel interessieren (keine direkte Reflektion), gibt es eine Möglichkeit ( [AttributeProvider]), Attribute eines vorhandenen Typs vorzuschlagen (um Doppelarbeit zu vermeiden), diese gilt jedoch nur für die Verwendung von Eigenschaften und Indexern.

Als Beispiel:

using System;
using System.ComponentModel;
class Foo {
    [AttributeProvider(typeof(IListSource))]
    public object Bar { get; set; }

    static void Main() {
        var bar = TypeDescriptor.GetProperties(typeof(Foo))["Bar"];
        foreach (Attribute attrib in bar.Attributes) {
            Console.WriteLine(attrib);
        }
    }
}

Ausgänge:

System.SerializableAttribute
System.ComponentModel.AttributeProviderAttribute
System.ComponentModel.EditorAttribute
System.Runtime.InteropServices.ComVisibleAttribute
System.Runtime.InteropServices.ClassInterfaceAttribute
System.ComponentModel.TypeConverterAttribute
System.ComponentModel.MergablePropertyAttribute
Marc Gravell
quelle
Bist du dir da sicher? Die MemberInfo.GetCustomAttributes-Methode verwendet ein Argument, das angibt, ob der Vererbungsbaum durchsucht werden soll.
Rune Grimstad
3
Hmm. Mir ist gerade aufgefallen, dass es bei der Frage darum geht, Attribute von einer Schnittstelle zu erben, nicht von einer Basisklasse.
Rune Grimstad
Gibt es dann einen Grund, Attribute auf Schnittstellen zu setzen?
Ryan Penfold
5
@ Ryan - sicher: zur Beschreibung der Schnittstelle. Zum Beispiel Serviceverträge.
Marc Gravell
3
Marc (und @Rune): Ja, im OP ging es um Schnittstellen. Aber der erste Satz Ihrer Antwort könnte verwirrend sein: "... oder überschreibende Mitglieder in einer abgeleiteten Klasse ..." - das ist nicht unbedingt wahr. Sie können Ihre Klasse Attribute von ihrer Basisklasse erben lassen. Das geht nicht nur mit Schnittstellen. Siehe auch: stackoverflow.com/questions/12106566/…
chiccodoro
39

Sie können eine nützliche Erweiterungsmethode definieren ...

Type type = typeof(ProjectController);
var attributes = type.GetCustomAttributes<CustomDescriptionAttribute>( true );

Hier ist die Erweiterungsmethode:

/// <summary>Searches and returns attributes. The inheritance chain is not used to find the attributes.</summary>
/// <typeparam name="T">The type of attribute to search for.</typeparam>
/// <param name="type">The type which is searched for the attributes.</param>
/// <returns>Returns all attributes.</returns>
public static T[] GetCustomAttributes<T>( this Type type ) where T : Attribute
{
  return GetCustomAttributes( type, typeof( T ), false ).Select( arg => (T)arg ).ToArray();
}

/// <summary>Searches and returns attributes.</summary>
/// <typeparam name="T">The type of attribute to search for.</typeparam>
/// <param name="type">The type which is searched for the attributes.</param>
/// <param name="inherit">Specifies whether to search this member's inheritance chain to find the attributes. Interfaces will be searched, too.</param>
/// <returns>Returns all attributes.</returns>
public static T[] GetCustomAttributes<T>( this Type type, bool inherit ) where T : Attribute
{
  return GetCustomAttributes( type, typeof( T ), inherit ).Select( arg => (T)arg ).ToArray();
}

/// <summary>Private helper for searching attributes.</summary>
/// <param name="type">The type which is searched for the attribute.</param>
/// <param name="attributeType">The type of attribute to search for.</param>
/// <param name="inherit">Specifies whether to search this member's inheritance chain to find the attribute. Interfaces will be searched, too.</param>
/// <returns>An array that contains all the custom attributes, or an array with zero elements if no attributes are defined.</returns>
private static object[] GetCustomAttributes( Type type, Type attributeType, bool inherit )
{
  if( !inherit )
  {
    return type.GetCustomAttributes( attributeType, false );
  }

  var attributeCollection = new Collection<object>();
  var baseType = type;

  do
  {
    baseType.GetCustomAttributes( attributeType, true ).Apply( attributeCollection.Add );
    baseType = baseType.BaseType;
  }
  while( baseType != null );

  foreach( var interfaceType in type.GetInterfaces() )
  {
    GetCustomAttributes( interfaceType, attributeType, true ).Apply( attributeCollection.Add );
  }

  var attributeArray = new object[attributeCollection.Count];
  attributeCollection.CopyTo( attributeArray, 0 );
  return attributeArray;
}

/// <summary>Applies a function to every element of the list.</summary>
private static void Apply<T>( this IEnumerable<T> enumerable, Action<T> function )
{
  foreach( var item in enumerable )
  {
    function.Invoke( item );
  }
}

Aktualisieren:

Hier ist eine kürzere Version, wie von SimonD in einem Kommentar vorgeschlagen:

private static IEnumerable<T> GetCustomAttributesIncludingBaseInterfaces<T>(this Type type)
{
  var attributeType = typeof(T);
  return type.GetCustomAttributes(attributeType, true).
    Union(type.GetInterfaces().
    SelectMany(interfaceType => interfaceType.GetCustomAttributes(attributeType, true))).
    Distinct().Cast<T>();
}
Tanascius
quelle
1
Dadurch werden nur Attribute auf Typebene abgerufen, keine Eigenschaften, Felder oder Elemente, oder?
Maslow
22
Sehr schön, ich persönlich verwende jetzt eine kürzere Version davon: private static IEnumerable <T> GetCustomAttributesIncludingBaseInterfaces <T> (dieser Typtyp) {var attributeType = typeof (T); return type.GetCustomAttributes (attributeType, true) .Union (type.GetInterfaces (). SelectMany (interfaceType => interfaceType.GetCustomAttributes (attributeType, true)). Distinct (). Cast <T> (); }
Simon D.
1
@ SimonD.: Und Ihre überarbeitete Lösung ist schneller.
Mynkow
1
@ SimonD das war eine Antwort wert, anstelle eines Kommentars.
Nick N.
Gibt es einen Grund, nicht durch Applyden eingebauten ForEachvonMicrosoft.Practices.ObjectBuilder2
Jacob Brewer
29

Ein Artikel von Brad Wilson dazu: Interface Attributes! = Class Attributes

Zusammenfassend: Klassen erben nicht von Schnittstellen, sondern implementieren sie. Dies bedeutet, dass die Attribute nicht automatisch Teil der Implementierung sind.

Wenn Sie Attribute erben müssen, verwenden Sie eine abstrakte Basisklasse anstelle einer Schnittstelle.

Roger Lipscombe
quelle
Was ist, wenn Sie mehrere Schnittstellen implementieren? Sie können diese Schnittstellen nicht einfach in abstrakte Klassen ändern, da C # in der Kategorie der Mehrfachvererbung fehlt.
Andy
10

Während eine C # -Klasse keine Attribute von ihren Schnittstellen erbt, gibt es eine nützliche Alternative beim Binden von Modellen in ASP.NET MVC3.

Wenn Sie das Modell der Ansicht als Schnittstelle und nicht als konkreten Typ deklarieren, wenden die Ansicht und der Modellordner die Attribute an (z. B. [Required]oder [DisplayName("Foo")]über die Schnittstelle beim Rendern und Validieren des Modells:

public interface IModel {
    [Required]
    [DisplayName("Foo Bar")]
    string FooBar { get; set; }
} 

public class Model : IModel {
    public string FooBar { get; set; }
}

Dann in der Ansicht:

@* Note use of interface type for the view model *@
@model IModel 

@* This control will receive the attributes from the interface *@
@Html.EditorFor(m => m.FooBar)
Peter Gluck
quelle
4

Dies ist eher für Personen gedacht, die Attribute aus Eigenschaften extrahieren möchten, die möglicherweise auf einer implementierten Schnittstelle vorhanden sind. Da diese Attribute nicht Teil der Klasse sind, erhalten Sie Zugriff darauf. Hinweis: Ich habe eine einfache Containerklasse, mit der Sie auf die PropertyInfo zugreifen können. Dafür habe ich sie benötigt. Hacke nach Bedarf. Das hat bei mir gut funktioniert.

public static class CustomAttributeExtractorExtensions
{
    /// <summary>
    /// Extraction of property attributes as well as attributes on implemented interfaces.
    /// This will walk up recursive to collect any interface attribute as well as their parent interfaces.
    /// </summary>
    /// <typeparam name="TAttributeType"></typeparam>
    /// <param name="typeToReflect"></param>
    /// <returns></returns>
    public static List<PropertyAttributeContainer<TAttributeType>> GetPropertyAttributesFromType<TAttributeType>(this Type typeToReflect)
        where TAttributeType : Attribute
    {
        var list = new List<PropertyAttributeContainer<TAttributeType>>();

        // Loop over the direct property members
        var properties = typeToReflect.GetProperties();

        foreach (var propertyInfo in properties)
        {
            // Get the attributes as well as from the inherited classes (true)
            var attributes = propertyInfo.GetCustomAttributes<TAttributeType>(true).ToList();
            if (!attributes.Any()) continue;

            list.AddRange(attributes.Select(attr => new PropertyAttributeContainer<TAttributeType>(attr, propertyInfo)));
        }

        // Look at the type interface declarations and extract from that type.
        var interfaces = typeToReflect.GetInterfaces();

        foreach (var @interface in interfaces)
        {
            list.AddRange(@interface.GetPropertyAttributesFromType<TAttributeType>());
        }

        return list;

    }

    /// <summary>
    /// Simple container for the Property and Attribute used. Handy if you want refrence to the original property.
    /// </summary>
    /// <typeparam name="TAttributeType"></typeparam>
    public class PropertyAttributeContainer<TAttributeType>
    {
        internal PropertyAttributeContainer(TAttributeType attribute, PropertyInfo property)
        {
            Property = property;
            Attribute = attribute;
        }

        public PropertyInfo Property { get; private set; }

        public TAttributeType Attribute { get; private set; }
    }
}
TravisWhidden
quelle
0

BEARBEITEN: Dies umfasst das Erben von Attributen von Schnittstellen auf Mitgliedern (inkl. Eigenschaften). Es gibt oben einfache Antworten für Typdefinitionen. Ich habe dies gerade gepostet, weil ich es als irritierende Einschränkung empfand und eine Lösung teilen wollte :)

Schnittstellen sind Mehrfachvererbungen und verhalten sich im Typsystem wie Vererbungen. Es gibt keinen guten Grund für solche Sachen. Reflexion ist ein bisschen hokey. Ich habe Kommentare hinzugefügt, um den Unsinn zu erklären.

(Dies ist .NET 3.5, da dies zufällig das ist, was das Projekt, das ich gerade mache, verwendet.)

// in later .NETs, you can cache reflection extensions using a static generic class and
// a ConcurrentDictionary. E.g.
//public static class Attributes<T> where T : Attribute
//{
//    private static readonly ConcurrentDictionary<MemberInfo, IReadOnlyCollection<T>> _cache =
//        new ConcurrentDictionary<MemberInfo, IReadOnlyCollection<T>>();
//
//    public static IReadOnlyCollection<T> Get(MemberInfo member)
//    {
//        return _cache.GetOrAdd(member, GetImpl, Enumerable.Empty<T>().ToArray());
//    }
//    //GetImpl as per code below except that recursive steps re-enter via the cache
//}

public static List<T> GetAttributes<T>(this MemberInfo member) where T : Attribute
{
    // determine whether to inherit based on the AttributeUsage
    // you could add a bool parameter if you like but I think it defeats the purpose of the usage
    var usage = typeof(T).GetCustomAttributes(typeof(AttributeUsageAttribute), true)
        .Cast<AttributeUsageAttribute>()
        .FirstOrDefault();
    var inherit = usage != null && usage.Inherited;

    return (
        inherit
            ? GetAttributesRecurse<T>(member)
            : member.GetCustomAttributes(typeof (T), false).Cast<T>()
        )
        .Distinct()  // interfaces mean duplicates are a thing
        // note: attribute equivalence needs to be overridden. The default is not great.
        .ToList();
}

private static IEnumerable<T> GetAttributesRecurse<T>(MemberInfo member) where T : Attribute
{
    // must use Attribute.GetCustomAttribute rather than MemberInfo.GetCustomAttribute as the latter
    // won't retrieve inherited attributes from base *classes*
    foreach (T attribute in Attribute.GetCustomAttributes(member, typeof (T), true))
        yield return attribute;

    // The most reliable target in the interface map is the property get method.
    // If you have set-only properties, you'll need to handle that case. I generally just ignore that
    // case because it doesn't make sense to me.
    PropertyInfo property;
    var target = (property = member as PropertyInfo) != null ? property.GetGetMethod() : member;

    foreach (var @interface in member.DeclaringType.GetInterfaces())
    {
        // The interface map is two aligned arrays; TargetMethods and InterfaceMethods.
        var map = member.DeclaringType.GetInterfaceMap(@interface);
        var memberIndex = Array.IndexOf(map.TargetMethods, target); // see target above
        if (memberIndex < 0) continue;

        // To recurse, we still need to hit the property on the parent interface.
        // Why don't we just use the get method from the start? Because GetCustomAttributes won't work.
        var interfaceMethod = property != null
            // name of property get method is get_<property name>
            // so name of parent property is substring(4) of that - this is reliable IME
            ? @interface.GetProperty(map.InterfaceMethods[memberIndex].Name.Substring(4))
            : (MemberInfo) map.InterfaceMethods[memberIndex];

        // Continuation is the word to google if you don't understand this
        foreach (var attribute in interfaceMethod.GetAttributes<T>())
            yield return attribute;
    }
}

Barebones NUnit-Test

[TestFixture]
public class GetAttributesTest
{
    [AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = true)]
    private sealed class A : Attribute
    {
        // default equality for Attributes is apparently semantic
        public override bool Equals(object obj)
        {
            return ReferenceEquals(this, obj);
        }

        public override int GetHashCode()
        {
            return base.GetHashCode();
        }
    }

    [AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = false)]
    private sealed class ANotInherited : Attribute { }

    public interface Top
    {
        [A, ANotInherited]
        void M();

        [A, ANotInherited]
        int P { get; }
    }

    public interface Middle : Top { }

    private abstract class Base
    {
        [A, ANotInherited]
        public abstract void M();

        [A, ANotInherited]
        public abstract int P { get; }
    }

    private class Bottom : Base, Middle
    {
        [A, ANotInherited]
        public override void M()
        {
            throw new NotImplementedException();
        }

        [A, ANotInherited]
        public override int P { get { return 42; } }
    }

    [Test]
    public void GetsAllInheritedAttributesOnMethods()
    {
        var attributes = typeof (Bottom).GetMethod("M").GetAttributes<A>();
        attributes.Should()
            .HaveCount(3, "there are 3 inherited copies in the class heirarchy and A is inherited");
    }

    [Test]
    public void DoesntGetNonInheritedAttributesOnMethods()
    {
        var attributes = typeof (Bottom).GetMethod("M").GetAttributes<ANotInherited>();
        attributes.Should()
            .HaveCount(1, "it shouldn't get copies of the attribute from base classes for a non-inherited attribute");
    }

    [Test]
    public void GetsAllInheritedAttributesOnProperties()
    {
        var attributes = typeof(Bottom).GetProperty("P").GetAttributes<A>();
        attributes.Should()
            .HaveCount(3, "there are 3 inherited copies in the class heirarchy and A is inherited");
    }

    [Test]
    public void DoesntGetNonInheritedAttributesOnProperties()
    {
        var attributes = typeof(Bottom).GetProperty("P").GetAttributes<ANotInherited>();
        attributes.Should()
            .HaveCount(1, "it shouldn't get copies of the attribute from base classes for a non-inherited attribute");
    }
}
Seth
quelle
0

Fügen Sie eine Schnittstelle mit Eigenschaften hinzu, deren Attribute / benutzerdefinierte Attribute denselben Eigenschaften wie die Klasse zugeordnet sind. Wir können die Schnittstelle der Klasse mithilfe der Visual Studio-Refactor-Funktion extrahieren. Lassen Sie diese Schnittstelle von einer Teilklasse implementieren.

Holen Sie sich jetzt das "Type" -Objekt des Klassenobjekts und rufen Sie mithilfe von getProperties on Type-Objekt benutzerdefinierte Attribute aus den Eigenschaftsinformationen ab. Dadurch werden die benutzerdefinierten Attribute für das Klassenobjekt nicht angegeben, da an die Klasseneigenschaften die benutzerdefinierten Attribute der Schnittstelleneigenschaften nicht angehängt / geerbt wurden.

Rufen Sie nun GetInterface (NameOfImplemetedInterfaceByclass) für das oben abgerufene Type-Objekt der Klasse auf. Dadurch wird das Objekt "Typ" der Schnittstelle bereitgestellt. Wir sollten den NAMEN der implementierten Schnittstelle kennen. Rufen Sie vom Typ-Objekt Eigenschaftsinformationen ab. Wenn an die Eigenschaft der Schnittstelle benutzerdefinierte Attribute angehängt sind, stellen die Eigenschaftsinformationen eine benutzerdefinierte Attributliste bereit. Die implementierende Klasse muss die Implementierung der Eigenschaften der Schnittstelle bereitgestellt haben. Passen Sie den spezifischen Eigenschaftsnamen des Klassenobjekts an die Liste der Eigenschaftsinformationen der Schnittstelle an, um die Liste der benutzerdefinierten Attribute abzurufen.

Das wird funktionieren.

user11432943
quelle
0

Obwohl meine Antwort zu spät und spezifisch für einen bestimmten Fall ist, möchte ich einige Ideen hinzufügen. Wie in anderen Antworten vorgeschlagen, würde Reflection oder andere Methoden dies tun.

In meinem Fall wurde in allen Modellen eine Eigenschaft (Zeitstempel) benötigt, um bestimmte Anforderungen (Attribut zur Überprüfung der Parallelität) in einem Entity Framework-Kernprojekt zu erfüllen. Wir könnten entweder [] vor allem Klasseneigenschaften hinzufügen (in der IModel-Schnittstelle hinzufügen, welche Modelle implementiert wurden, hat nicht funktioniert). Durch die Fluent API habe ich jedoch Zeit gespart, was in diesen Fällen hilfreich ist. In der fließenden API kann ich in allen Modellen nach bestimmten Eigenschaftsnamen suchen und in einer Zeile IsConcurrencyToken () festlegen !!

var props = from e in modelBuilder.Model.GetEntityTypes()
            from p in e.GetProperties()
            select p;
props.Where(p => p.PropertyInfo.Name == "ModifiedTime").ToList().ForEach(p => { p.IsConcurrencyToken = true; });

Wenn Sie in Hunderten von Klassen / Modellen ein Attribut zum selben Eigenschaftsnamen hinzufügen müssen, können wir auch fließende API-Methoden für den eingebauten oder benutzerdefinierten Attribut-Resolver verwenden. Obwohl die fließende API von EF (sowohl Core als auch EF6) möglicherweise Reflexion hinter den Kulissen verwendet, können wir Mühe sparen :)

Prasanna Venkatanathan
quelle