// // ContractDescriptionGenerator.cs // // Author: // Atsushi Enomoto // Atsushi Enomoto // // Copyright (C) 2005-2007 Novell, Inc. http://www.novell.com // Copyright (C) 2011 Xamarin, Inc. http://xamarin.com // // Permission is hereby granted, free of charge, to any person obtaining // a copy of this software and associated documentation files (the // "Software"), to deal in the Software without restriction, including // without limitation the rights to use, copy, modify, merge, publish, // distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to // the following conditions: // // The above copyright notice and this permission notice shall be // included in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // using System; using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Net.Security; using System.Reflection; using System.Runtime.Serialization; using System.ServiceModel; using System.ServiceModel.Channels; namespace System.ServiceModel.Description { internal static class ContractDescriptionGenerator { public delegate bool GetOperationContractAttributeExtender (MethodBase method, object[] customAttributes, ref OperationContractAttribute oca); static List getOperationContractAttributeExtenders; public static void RegisterGetOperationContractAttributeExtender (GetOperationContractAttributeExtender extender) { if (extender == null) return; if (getOperationContractAttributeExtenders == null) getOperationContractAttributeExtenders = new List (); if (getOperationContractAttributeExtenders.Contains (extender)) return; getOperationContractAttributeExtenders.Add (extender); } public static OperationContractAttribute GetOperationContractAttribute (MethodBase method) { object [] matts = method.GetCustomAttributes (typeof (OperationContractAttribute), false); OperationContractAttribute oca; if (matts.Length == 0) oca = null; else oca = matts [0] as OperationContractAttribute; if (getOperationContractAttributeExtenders != null && getOperationContractAttributeExtenders.Count > 0) { foreach (var extender in getOperationContractAttributeExtenders) if (extender (method, matts, ref oca)) break; } return oca; } static void GetServiceContractAttribute (Type type, Dictionary table) { for (; type != null; type = type.BaseType) { foreach (ServiceContractAttribute i in type.GetCustomAttributes ( typeof (ServiceContractAttribute), true)) table [type] = i; foreach (Type t in type.GetInterfaces ()) GetServiceContractAttribute (t, table); } } public static Dictionary GetServiceContractAttributes (Type type) { Dictionary table = new Dictionary (); GetServiceContractAttribute (type, table); return table; } public static ContractDescription GetContract (Type contractType) { return GetContract (contractType, (Type) null); } public static ContractDescription GetContract ( Type contractType, object serviceImplementation) { if (serviceImplementation == null) throw new ArgumentNullException ("serviceImplementation"); return GetContract (contractType, serviceImplementation.GetType ()); } public static MessageContractAttribute GetMessageContractAttribute (Type type) { for (Type t = type; t != null; t = t.BaseType) { object [] matts = t.GetCustomAttributes ( typeof (MessageContractAttribute), true); if (matts.Length > 0) return (MessageContractAttribute) matts [0]; } return null; } public static ContractDescription GetCallbackContract (Type serviceType, Type callbackType) { return GetContract (callbackType, null, serviceType); } public static ContractDescription GetContract ( Type givenContractType, Type givenServiceType) { return GetContract (givenContractType, givenServiceType, null); } static ContractDescription GetContract (Type givenContractType, Type givenServiceType, Type serviceTypeForCallback) { var ret = GetContractInternal (givenContractType, givenServiceType, serviceTypeForCallback); if (ret == null) throw new InvalidOperationException (String.Format ("Attempted to get contract type from '{0}' which neither is a service contract nor does it inherit service contract.", serviceTypeForCallback ?? givenContractType)); return ret; } internal static Type GetContractAssignableToInterfaces(Type given, Type [] givenInterfaces) { if ((given != null) && (givenInterfaces.Count() < 1)) return null; Dictionary interfaceGraph = new Dictionary (); foreach (var smaller in givenInterfaces) { interfaceGraph [smaller] = 0; foreach (var bigger in givenInterfaces) { if (smaller.IsAssignableFrom (bigger)) { interfaceGraph [smaller]++; } } } List possibleInterfaces = new List (); foreach (var node in interfaceGraph) { if (node.Value == 1) { possibleInterfaces.Add(node.Key); } } // For this purpose a contract is a set of interfaces. it is necessary to find the interface representative of rest of the set. It will be assignable to all of // the others but can only be assigned to by itself. This will give it count of ! in its slot in the interfaceGraph. To be a valid set there can be only one with a count of one // in its slot. More results in InvalidOperation exceptioni with ambigours error message, less means that the interface we want is the one passed in by the parameter given // and by returning null we give the callign method permission to use it.. switch (possibleInterfaces.Count()) { case 0: break; case 1: return possibleInterfaces [0]; break; default: if (!given.IsInterface) throw new InvalidOperationException ("The contract type of " + given + " is ambiguous: can be either " + possibleInterfaces[0] + " or " + possibleInterfaces[1]); break; } return null; } internal static ContractDescription GetContractInternal (Type givenContractType, Type givenServiceType, Type serviceTypeForCallback) { if (givenContractType == null) throw new ArgumentNullException ("givenContractType"); // FIXME: serviceType should be used for specifying attributes like OperationBehavior. Type exactContractType = null; ServiceContractAttribute sca = null; Dictionary contracts = GetServiceContractAttributes (serviceTypeForCallback ?? givenServiceType ?? givenContractType); if (contracts.ContainsKey (givenContractType)) { exactContractType = givenContractType; sca = contracts [givenContractType]; } else { Type[] contractTypes = contracts.Keys.ToArray() as Type []; exactContractType = GetContractAssignableToInterfaces(givenContractType, contractTypes); if (exactContractType != null) sca = contracts[exactContractType]; } if (exactContractType == null) exactContractType = givenContractType; if (sca == null) { if (serviceTypeForCallback != null) sca = contracts.Values.First (); else return null; // no contract } string name = sca.Name ?? exactContractType.Name; string ns = sca.Namespace ?? "http://tempuri.org/"; ContractDescription cd = new ContractDescription (name, ns); cd.ContractType = exactContractType; cd.CallbackContractType = sca.CallbackContract; cd.SessionMode = sca.SessionMode; if (sca.ConfigurationName != null) cd.ConfigurationName = sca.ConfigurationName; else cd.ConfigurationName = exactContractType.FullName; if (sca.HasProtectionLevel) cd.ProtectionLevel = sca.ProtectionLevel; /* * Calling `FillOperationsForInterface(cd, X, null, false)' followed by * `FillOperationsForInterface(cd, X, Y, false)' would attempt to populate * the behavior list for 'X' twice (bug #6187). * * Therefor, we manually iterate over the list of interfaces here instead of * using ContractDescription.GetInheritedContracts(). * */ var inherited = new Collection (); var interfaces = cd.ContractType.GetInterfaces () .Where (t => t != givenContractType); foreach (var it in interfaces ) { var icd = GetContractInternal (it, givenServiceType, null); if (icd != null) inherited.Add (icd); } foreach (var icd in inherited) { foreach (var od in icd.Operations) { if (!cd.Operations.Any (o => o.Name == od.Name && o.SyncMethod == od.SyncMethod && o.BeginMethod == od.BeginMethod && o.InCallbackContract == od.InCallbackContract)) cd.Operations.Add (od); } } FillOperationsForInterface (cd, cd.ContractType, givenServiceType, false); if (cd.CallbackContractType != null) FillOperationsForInterface (cd, cd.CallbackContractType, null, true); // FIXME: enable this when I found where this check is needed. /* if (cd.Operations.Count == 0) throw new InvalidOperationException (String.Format ("The service contract type {0} has no operation. At least one operation must exist.", contractType)); */ return cd; } static void FillOperationsForInterface (ContractDescription cd, Type exactContractType, Type givenServiceType, bool isCallback) { // FIXME: load Behaviors MethodInfo [] contractMethods = /*exactContractType.IsInterface ? GetAllMethods (exactContractType) :*/ exactContractType.GetMethods (BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly); MethodInfo [] serviceMethods = contractMethods; if (givenServiceType != null && exactContractType.IsInterface) { var l = new List (); foreach (Type t in GetAllInterfaceTypes (exactContractType)) l.AddRange (givenServiceType.GetInterfaceMap (t).TargetMethods); serviceMethods = l.ToArray (); } for (int i = 0; i < contractMethods.Length; ++i) { MethodInfo mi = contractMethods [i]; OperationContractAttribute oca = GetOperationContractAttribute (mi); if (oca == null) continue; MethodInfo end = null; if (oca.AsyncPattern) { if (String.Compare ("Begin", 0, mi.Name,0, 5) != 0) throw new InvalidOperationException ("For async operation contract patterns, the initiator method name must start with 'Begin'."); string endName = "End" + mi.Name.Substring (5); end = mi.DeclaringType.GetMethod (endName); if (end == null) throw new InvalidOperationException (String.Format ("'{0}' method is missing. For async operation contract patterns, corresponding End method is required for each Begin method.", endName)); if (GetOperationContractAttribute (end) != null) throw new InvalidOperationException ("Async 'End' method must not have OperationContractAttribute. It is automatically treated as the EndMethod of the corresponding 'Begin' method."); } OperationDescription od = GetOrCreateOperation (cd, mi, serviceMethods [i], oca, end, isCallback, givenServiceType); if (end != null) od.EndMethod = end; } } static MethodInfo [] GetAllMethods (Type type) { var l = new List (); foreach (var t in GetAllInterfaceTypes (type)) { #if FULL_AOT_RUNTIME // The MethodBase[] from t.GetMethods () is cast to a IEnumerable // when passed to List.AddRange, which in turn casts it to // ICollection . The full-aot compiler has no idea of this, so // we're going to make it aware. int c = ((ICollection ) t.GetMethods ()).Count; #endif l.AddRange (t.GetMethods ()); } return l.ToArray (); } static IEnumerable GetAllInterfaceTypes (Type type) { yield return type; foreach (var t in type.GetInterfaces ()) foreach (var tt in GetAllInterfaceTypes (t)) yield return tt; } static OperationDescription GetOrCreateOperation ( ContractDescription cd, MethodInfo mi, MethodInfo serviceMethod, OperationContractAttribute oca, MethodInfo endMethod, bool isCallback, Type givenServiceType) { string name = oca.Name ?? (oca.AsyncPattern ? mi.Name.Substring (5) : mi.Name); OperationDescription od = cd.Operations.FirstOrDefault (o => o.Name == name && o.InCallbackContract == isCallback); if (od == null) { od = new OperationDescription (name, cd); od.IsOneWay = oca.IsOneWay; if (oca.HasProtectionLevel) od.ProtectionLevel = oca.ProtectionLevel; if (HasInvalidMessageContract (mi, oca.AsyncPattern)) throw new InvalidOperationException (String.Format ("The operation {0} contains more than one parameters and one or more of them are marked with MessageContractAttribute, but the attribute must be used within an operation that has only one parameter.", od.Name)); var xfa = serviceMethod.GetCustomAttribute (false); if (xfa != null) od.Behaviors.Add (new XmlSerializerOperationBehavior (od, xfa)); var dfa = serviceMethod.GetCustomAttribute (false); if (dfa != null) od.Behaviors.Add (new DataContractSerializerOperationBehavior (od, dfa)); od.Messages.Add (GetMessage (od, mi, oca, true, isCallback, null)); if (!od.IsOneWay) { var asyncReturnType = endMethod != null ? endMethod.ReturnType : null; var md = GetMessage (od, endMethod ?? mi, oca, false, isCallback, asyncReturnType); od.Messages.Add (md); var mpa = mi.ReturnParameter.GetCustomAttribute (true); if (mpa != null) { var mpd = md.Body.Parts.FirstOrDefault (pd => pd.Name == mpa.Name); if (mpd != null) { md.Body.Parts.Remove (mpd); md.Body.ReturnValue = mpd; mpd.Name = mpa.Name; } else if (md.Body.ReturnValue == null) throw new InvalidOperationException (String.Format ("Specified message part '{0}' in MessageParameterAttribute on the return value, was not found", mpa.Name)); } } var knownTypeAtts = cd.ContractType.GetCustomAttributes (typeof (ServiceKnownTypeAttribute), false).Union ( mi.GetCustomAttributes (typeof (ServiceKnownTypeAttribute), false)).Union ( serviceMethod.GetCustomAttributes (typeof (ServiceKnownTypeAttribute), false)); foreach (ServiceKnownTypeAttribute a in knownTypeAtts) foreach (Type t in a.GetTypes (givenServiceType)) od.KnownTypes.Add (t); foreach (FaultContractAttribute a in mi.GetCustomAttributes (typeof (FaultContractAttribute), false)) { var fname = a.Name ?? a.DetailType.Name + "Fault"; var fns = a.Namespace ?? cd.Namespace; var fd = new FaultDescription (a.Action ?? cd.Namespace + cd.Name + "/" + od.Name + fname) { DetailType = a.DetailType, Name = fname, Namespace = fns }; #if !MOBILE if (a.HasProtectionLevel) fd.ProtectionLevel = a.ProtectionLevel; #endif od.Faults.Add (fd); } cd.Operations.Add (od); } else if ((oca.AsyncPattern && od.BeginMethod != null && od.BeginMethod != mi || !oca.AsyncPattern && od.SyncMethod != null && od.SyncMethod != mi) && od.InCallbackContract == isCallback) throw new InvalidOperationException (String.Format ("contract '{1}' cannot have two operations for '{0}' that have the identical names and different set of parameters.", name, cd.Name)); if (oca.AsyncPattern) od.BeginMethod = mi; else od.SyncMethod = mi; od.IsInitiating = oca.IsInitiating; od.IsTerminating = oca.IsTerminating; if (mi != serviceMethod) foreach (object obj in mi.GetCustomAttributes (typeof (IOperationBehavior), true)) od.Behaviors.Add ((IOperationBehavior) obj); if (serviceMethod != null) { foreach (object obj in serviceMethod.GetCustomAttributes (typeof(IOperationBehavior),true)) od.Behaviors.Add ((IOperationBehavior) obj); } #if !MOBILE if (od.Behaviors.Find() == null) od.Behaviors.Add (new OperationBehaviorAttribute ()); #endif // FIXME: fill KnownTypes, Behaviors and Faults. if (isCallback) od.InCallbackContract = true; else od.InOrdinalContract = true; return od; } static bool HasInvalidMessageContract (MethodInfo mi, bool async) { var pars = mi.GetParameters (); if (async) { if (pars.Length > 3) { if (pars.Take (pars.Length - 2).Any (par => par.ParameterType.GetCustomAttribute (true) != null)) return true; } } else { if (pars.Length > 1) { if (pars.Any (par => par.ParameterType.GetCustomAttribute (true) != null)) return true; } } return false; } static MessageDescription GetMessage ( OperationDescription od, MethodInfo mi, OperationContractAttribute oca, bool isRequest, bool isCallback, Type asyncReturnType) { ContractDescription cd = od.DeclaringContract; ParameterInfo [] plist = mi.GetParameters (); Type messageType = null; string action = isRequest ? oca.Action : oca.ReplyAction; MessageContractAttribute mca; Type retType = asyncReturnType; if (!isRequest && retType == null) retType = mi.ReturnType; // If the argument is only one and has [MessageContract] // then infer it as a typed messsage if (isRequest) { int len = mi.Name.StartsWith ("Begin", StringComparison.Ordinal) ? 3 : 1; mca = plist.Length != len ? null : GetMessageContractAttribute (plist [0].ParameterType); if (mca != null) messageType = plist [0].ParameterType; } else { mca = GetMessageContractAttribute (retType); if (mca != null) messageType = retType; } if (action == null) action = String.Concat (cd.Namespace, cd.Namespace.Length == 0 ? "urn:" : cd.Namespace.EndsWith ("/") ? "" : "/", cd.Name, "/", od.Name, isRequest ? String.Empty : "Response"); MessageDescription md; if (mca != null) md = CreateMessageDescription (messageType, cd.Namespace, action, isRequest, isCallback, mca); else md = CreateMessageDescription (oca, plist, od.Name, cd.Namespace, action, isRequest, isCallback, retType); // ReturnValue if (!isRequest) { MessagePartDescription mp = CreatePartCore (GetMessageParameterAttribute (mi.ReturnTypeCustomAttributes), od.Name + "Result", md.Body.WrapperNamespace); mp.Index = 0; mp.Type = mca != null ? typeof (void) : retType; md.Body.ReturnValue = mp; } return md; } public static MessageDescription CreateMessageDescription ( Type messageType, string defaultNamespace, string action, bool isRequest, bool isCallback, MessageContractAttribute mca) { MessageDescription md = new MessageDescription (action, isRequest ^ isCallback ? MessageDirection.Input : MessageDirection.Output) { IsRequest = isRequest }; md.MessageType = MessageFilterOutByRef (messageType); if (mca.HasProtectionLevel) md.ProtectionLevel = mca.ProtectionLevel; MessageBodyDescription mb = md.Body; if (mca.IsWrapped) { mb.WrapperName = mca.WrapperName ?? messageType.Name; mb.WrapperNamespace = mca.WrapperNamespace ?? defaultNamespace; } foreach (MemberInfo bmi in messageType.GetMembers (BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) { Type mtype = null; string mname = null; if (bmi is FieldInfo) { FieldInfo fi = (FieldInfo) bmi; mtype = fi.FieldType; mname = fi.Name; } else if (bmi is PropertyInfo) { PropertyInfo pi = (PropertyInfo) bmi; mtype = pi.PropertyType; mname = pi.Name; } else continue; var mha = bmi.GetCustomAttribute (false); if (mha != null) { var pd = CreateHeaderDescription (mha, mname, defaultNamespace); pd.Type = MessageFilterOutByRef (mtype); pd.MemberInfo = bmi; md.Headers.Add (pd); } var mpa = bmi.GetCustomAttribute (false); if (mpa != null) { var pd = new MessagePropertyDescription (mpa.Name ?? mname); pd.Type = MessageFilterOutByRef (mtype); pd.MemberInfo = bmi; md.Properties.Add (pd); } var mba = GetMessageBodyMemberAttribute (bmi); if (mba != null) { var pd = CreatePartCore (mba, mname, defaultNamespace); pd.Type = MessageFilterOutByRef (mtype); pd.MemberInfo = bmi; mb.Parts.Add (pd); } } int index = 0; foreach (var part in mb.Parts) part.Index = index++; return md; } public static MessageDescription CreateMessageDescription ( OperationContractAttribute oca, ParameterInfo[] plist, string name, string defaultNamespace, string action, bool isRequest, bool isCallback, Type retType) { var dir = isRequest ^ isCallback ? MessageDirection.Input : MessageDirection.Output; MessageDescription md = new MessageDescription (action, dir) { IsRequest = isRequest }; MessageBodyDescription mb = md.Body; mb.WrapperName = name + (isRequest ? String.Empty : "Response"); mb.WrapperNamespace = defaultNamespace; if (oca.HasProtectionLevel) md.ProtectionLevel = oca.ProtectionLevel; // Parts int index = 0; foreach (ParameterInfo pi in plist) { // AsyncCallback and state are extraneous. if (oca.AsyncPattern) { if (isRequest && pi.Position == plist.Length - 2) break; if (!isRequest && pi.Position == plist.Length - 1) break; } // They are ignored: // - out parameter in request // - neither out nor ref parameter in reply if (isRequest && pi.IsOut) continue; if (!isRequest && !pi.IsOut && !pi.ParameterType.IsByRef) continue; MessagePartDescription pd = CreatePartCore (GetMessageParameterAttribute (pi), pi.Name, defaultNamespace); pd.Index = index++; pd.Type = MessageFilterOutByRef (pi.ParameterType); mb.Parts.Add (pd); } return md; } // public static void FillMessageBodyDescriptionByContract ( // Type messageType, MessageBodyDescription mb) // { // } static MessageHeaderDescription CreateHeaderDescription (MessageHeaderAttribute mha, string defaultName, string defaultNamespace) { var ret = CreatePartCore (mha, defaultName, defaultNamespace, delegate (string n, string ns) { return new MessageHeaderDescription (n, ns); }); ret.Actor = mha.Actor; ret.MustUnderstand = mha.MustUnderstand; ret.Relay = mha.Relay; return ret; } static MessagePartDescription CreatePartCore ( MessageParameterAttribute mpa, string defaultName, string defaultNamespace) { string pname = null; if (mpa != null && mpa.Name != null) pname = mpa.Name; if (pname == null) pname = defaultName; return new MessagePartDescription (pname, defaultNamespace); } static MessagePartDescription CreatePartCore (MessageBodyMemberAttribute mba, string defaultName, string defaultNamespace) { var ret = CreatePartCore (mba, defaultName, defaultNamespace, delegate (string n, string ns) { return new MessagePartDescription (n, ns); }); ret.Index = mba.Order; return ret; } static T CreatePartCore (MessageContractMemberAttribute mba, string defaultName, string defaultNamespace, Func creator) { string pname = null, pns = null; if (mba != null) { if (mba.Name != null) pname = mba.Name; if (mba.Namespace != null) pns = mba.Namespace; } if (pname == null) pname = defaultName; if (pns == null) pns = defaultNamespace; return creator (pname, pns); } static Type MessageFilterOutByRef (Type type) { return type == null ? null : type.IsByRef ? type.GetElementType () : type; } static MessageParameterAttribute GetMessageParameterAttribute (ICustomAttributeProvider provider) { object [] attrs = provider.GetCustomAttributes ( typeof (MessageParameterAttribute), true); return attrs.Length > 0 ? (MessageParameterAttribute) attrs [0] : null; } static MessageBodyMemberAttribute GetMessageBodyMemberAttribute (MemberInfo mi) { object [] matts = mi.GetCustomAttributes ( typeof (MessageBodyMemberAttribute), true); return matts.Length > 0 ? (MessageBodyMemberAttribute) matts [0] : null; } } }