/// <summary> /// Initialize this class via reflection /// </summary> private void InitializeViaReflection(ServiceEndpointOptions service) { // Is there an options() method that can be called? List <KeyValuePair <String, Type> > resourceTypes = null; var optionsMethod = service.Behavior.Type.GetRuntimeMethod("Options", Type.EmptyTypes); ServiceOptions serviceOptions = null; if (optionsMethod != null) { var behaviorInstance = Activator.CreateInstance(service.Behavior.Type); serviceOptions = optionsMethod.Invoke(behaviorInstance, null) as ServiceOptions; if (serviceOptions != null) // Remove unused resources { resourceTypes = serviceOptions.Resources.Select(o => new KeyValuePair <string, Type>(o.ResourceName, o.ResourceType)).ToList(); } } else { // Resource types resourceTypes = service.Contracts.SelectMany(c => c.Type.GetCustomAttributes <ServiceKnownResourceAttribute>().Select(o => new KeyValuePair <String, Type>(o.Type.GetCustomAttribute <XmlRootAttribute>()?.ElementName, o.Type)).Where(o => !String.IsNullOrEmpty(o.Key))).ToList(); } // Construct the paths var operations = service.Contracts.SelectMany(c => c.Type.GetMethods(BindingFlags.Public | BindingFlags.Instance) .Select(o => new { Rest = o.GetCustomAttribute <RestInvokeAttribute>(), ContractMethod = o, BehaviorMethod = service.Behavior.Type.GetMethod(o.Name, o.GetParameters().Select(p => p.ParameterType).ToArray()) })) .Where(o => o.Rest != null); // Create tags if (operations.Any(o => o.Rest.UriTemplate.Contains("{resourceType}"))) { this.Tags = resourceTypes.Select(o => new SwaggerTag(o.Key, MetadataComposerUtil.GetElementDocumentation(o.Value, MetaDataElementType.Summary))).ToList(); } // Process operations foreach (var operation in operations.GroupBy(o => o.Rest.UriTemplate.StartsWith("/") ? o.Rest.UriTemplate : "/" + o.Rest.UriTemplate)) { // If the path does not contain {resourceType} and there are no ServiceKnownTypes then proceed var path = new SwaggerPath(); foreach (var val in operation) { // Process operations var pathDefinition = new SwaggerPathDefinition(val.BehaviorMethod, val.ContractMethod); path.Add(val.Rest.Method.ToLower(), pathDefinition); if (pathDefinition.Consumes.Count == 0) { pathDefinition.Consumes.AddRange(this.Consumes); } if (pathDefinition.Produces.Count == 0) { pathDefinition.Produces.AddRange(this.Produces); } // Any faults? foreach (var flt in service.Contracts.SelectMany(t => t.Type.GetCustomAttributes <ServiceFaultAttribute>())) { pathDefinition.Responses.Add(flt.StatusCode, new SwaggerSchemaElement() { Description = flt.Condition, Schema = new SwaggerSchemaDefinition() { Reference = $"#/definitions/{MetadataComposerUtil.CreateSchemaReference(flt.FaultType)}", NetType = flt.FaultType } }); } } if (operation.Key.Contains("{resourceType}")) { foreach (var resource in resourceTypes) { var resourcePath = operation.Key.Replace("{resourceType}", resource.Key); if (this.Paths.ContainsKey(resourcePath) || resourcePath.Contains("history") && !typeof(IVersionedEntity).IsAssignableFrom(resource.Value)) { continue; } // Create a copy of the path and customize it to the resource List <String> unsupportedVerbs = new List <string>(); // Get the resource options for this resource ServiceResourceOptions resourceOptions = serviceOptions?.Resources.FirstOrDefault(o => o.ResourceName == resource.Key); var subPath = new SwaggerPath(path); foreach (var v in subPath) { // Check that this resource is supported var resourceCaps = resourceOptions?.Capabilities.FirstOrDefault(c => c.Capability == MetadataComposerUtil.VerbToCapability(v.Key, v.Value.Parameters.Count)); if (resourceOptions != null && resourceCaps == null && v.Key != "head" && v.Key != "options" && v.Key != "search") { unsupportedVerbs.Add(v.Key); continue; } // Add tags for resource v.Value.Tags.Add(resource.Key); v.Value.Parameters.RemoveAll(o => o.Name == "resourceType"); // Security? if (this.SecurityDefinitions.Count > 0 && resourceCaps?.Demand.Length > 0) { v.Value.Security = new List <SwaggerPathSecurity>() { new SwaggerPathSecurity() { { "oauth_user", resourceCaps.Demand.Distinct().ToList() } } }; } // Query parameters? if ((v.Key == "get" || v.Key == "head")) { // Build query parameters v.Value.Parameters.AddRange(resource.Value.GetProperties(BindingFlags.Instance | BindingFlags.Public) .Where(o => o.GetCustomAttributes <XmlElementAttribute>().Any() || o.GetCustomAttribute <QueryParameterAttribute>() != null) .Select(o => new SwaggerParameter(o)) ); //v.Value.Parameters.AddRange(new SwaggerParameter[] //{ // new SwaggerParameter() // { // Name = "_offset", // Type = SwaggerSchemaElementType.number, // Description = "Offset of query results", // Location = SwaggerParameterLocation.query // }, // new SwaggerParameter() // { // Name = "_count", // Type = SwaggerSchemaElementType.number, // Description = "Count of query results to include in result set", // Location = SwaggerParameterLocation.query // }, // new SwaggerParameter() // { // Name = "_lean", // Type = SwaggerSchemaElementType.boolean, // Description = "When true, the server will only return minimal data by removing duplicates from the bundle's resource[] property. NOTE: This means that the .count parameter may not match the number of items in resource[], however you should continue to use the .count parameter to increase your offset", // Location = SwaggerParameterLocation.query // }, // new SwaggerParameter() // { // Name = "_orderBy", // Type = SwaggerSchemaElementType.@string, // Description = "Indicates a series of parameters to order the result set by", // Location = SwaggerParameterLocation.query // }, // new SwaggerParameter() // { // Name = "_queryId", // Type = SwaggerSchemaElementType.@string, // Description = "The unique identifier for the query (for continuation)", // Location = SwaggerParameterLocation.query // }, // new SwaggerParameter() // { // Name = "_viewModel", // Type = SwaggerSchemaElementType.@string, // Description = "When using the view-model content-type, the view model to use", // Location = SwaggerParameterLocation.query, // Enum = new List<string>() // { // "min", // "max" // } // } //}); } // Replace the response if necessary var resourceSchemaRef = new SwaggerSchemaDefinition() { NetType = resource.Value, Reference = $"#/definitions/{MetadataComposerUtil.CreateSchemaReference(resource.Value)}" }; SwaggerSchemaElement schema = null; if (v.Value.Responses.TryGetValue(200, out schema)) { schema.Schema = resourceSchemaRef; } if (v.Value.Responses.TryGetValue(201, out schema)) { schema.Schema = resourceSchemaRef; } // Replace the body if necessary var bodyParm = v.Value.Parameters.FirstOrDefault(o => o.Location == SwaggerParameterLocation.body && o.Schema?.NetType?.IsAssignableFrom(resource.Value) == true); if (bodyParm != null) { bodyParm.Schema = resourceSchemaRef; } } // foreach subpath // Add the resource path? foreach (var nv in unsupportedVerbs) { subPath.Remove(nv); } // Child resources? we want to multiply them if so if ((resourcePath.Contains("{childResourceType}") || resourcePath.Contains("{operationName}")) && resourceOptions.ChildResources.Count > 0) { foreach (var sp in subPath) { sp.Value.Parameters.RemoveAll(o => o.Name == "childResourceType" || o.Name == "operationName"); } // Correct for child resources - rewriting the schema and opts foreach (var cp in resourceOptions.ChildResources) { // Bound to if (resourcePath.Contains("{id}") && !cp.Scope.HasFlag(ChildObjectScopeBinding.Instance) || !resourcePath.Contains("{id}") && !cp.Scope.HasFlag(ChildObjectScopeBinding.Class) || resourcePath.Contains("$") && !cp.Classification.HasFlag(ChildObjectClassification.RpcOperation) || !resourcePath.Contains("$") && !cp.Classification.HasFlag(ChildObjectClassification.Resource)) { continue; } var newPath = new SwaggerPath(); foreach (var v in subPath) { var childCaps = cp.Capabilities.FirstOrDefault(c => c.Capability == MetadataComposerUtil.VerbToCapability(v.Key, v.Value.Parameters.Count)); if (childCaps != null) { newPath.Add(v.Key, v.Value); } } this.Paths.Add(resourcePath.Replace("{childResourceType}", cp.ResourceName).Replace("{operationName}", cp.ResourceName), newPath); foreach (var sp in newPath) { // Replace the response if necessary var resourceSchemaRef = new SwaggerSchemaDefinition() { NetType = cp.ResourceType, Reference = $"#/definitions/{MetadataComposerUtil.CreateSchemaReference(cp.ResourceType)}" }; SwaggerSchemaElement schema = null; if (sp.Value.Responses.TryGetValue(200, out schema)) { schema.Schema = resourceSchemaRef; } if (sp.Value.Responses.TryGetValue(201, out schema)) { schema.Schema = resourceSchemaRef; } // Replace the body if necessary var bodyParm = sp.Value.Parameters.FirstOrDefault(o => o.Location == SwaggerParameterLocation.body && o.Schema?.NetType?.IsAssignableFrom(resource.Value) == true); if (bodyParm != null) { bodyParm.Schema = resourceSchemaRef; } } } } else if (!resourcePath.Contains("{childResourceType}")) { this.Paths.Add(resourcePath, subPath); } } } else { this.Paths.Add(operation.Key, path); } } }
/// <summary> /// Create new swagger document /// </summary> public SwaggerDocument(ServiceEndpointOptions service) : this() { var listen = new Uri(service.BaseUrl.First()); this.BasePath = listen.AbsolutePath; listen = new Uri(ApplicationServiceContext.Current.GetService <IConfigurationManager>().GetSection <MetadataConfigurationSection>().ApiHost ?? listen.ToString()); this.Info = new SwaggerServiceInfo() { Title = MetadataComposerUtil.GetElementDocumentation(service.Behavior.Type, MetaDataElementType.Summary) ?? service.Behavior.Type.GetCustomAttribute <ServiceBehaviorAttribute>()?.Name, Description = MetadataComposerUtil.GetElementDocumentation(service.Behavior.Type, MetaDataElementType.Remarks), Version = $"{service.Behavior.Type.Assembly.GetName().Version.ToString()} ({service.Behavior.Type.Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion})" }; this.Host = $"{listen.Host}:{listen.Port}".Replace("0.0.0.0", RestOperationContext.Current.IncomingRequest.Url.Host); // Get the schemes this.Schemes = new List <string>() { listen.Scheme }; // Construct the paths var operations = service.Contracts.SelectMany(c => c.Type.GetMethods(BindingFlags.Public | BindingFlags.Instance) .Select(o => new { Rest = o.GetCustomAttribute <RestInvokeAttribute>(), ContractMethod = o, BehaviorMethod = service.Behavior.Type.GetMethod(o.Name, o.GetParameters().Select(p => p.ParameterType).ToArray()) })) .Where(o => o.Rest != null); // What this option produces this.Produces.AddRange(service.Contracts.SelectMany(c => c.Type.GetCustomAttributes <ServiceProducesAttribute>().Select(o => o.MimeType))); this.Consumes.AddRange(service.Contracts.SelectMany(c => c.Type.GetCustomAttributes <ServiceConsumesAttribute>().Select(o => o.MimeType))); // Security requires us to peek into the runtime environment ... var serviceCaps = MetadataComposerUtil.GetServiceCapabilities(service.ServiceType); if (serviceCaps.HasFlag(ServiceEndpointCapabilities.BearerAuth)) { var bauth = AuthenticationContext.Current; var tokenUrl = new Uri(MetadataComposerUtil.ResolveService("acs").BaseUrl.FirstOrDefault()); if (tokenUrl.Host == "0.0.0.0") // Host is vanialla { tokenUrl = new Uri($"{listen.Scheme}://{listen.Host}:{listen.Port}{tokenUrl.AbsolutePath}"); } AuthenticationContext.Current = new AuthenticationContext(AuthenticationContext.SystemPrincipal); this.SecurityDefinitions = new Dictionary <string, SwaggerSecurityDefinition>() { { "oauth_user", new SwaggerSecurityDefinition() { Flow = SwaggerSecurityFlow.password, Scopes = ApplicationServiceContext.Current.GetService <IRepositoryService <SecurityPolicy> >()?.Find(o => o.ObsoletionTime == null).ToDictionary(o => o.Oid, o => o.Name), TokenUrl = $"{tokenUrl.ToString().Replace("0.0.0.0", RestOperationContext.Current.IncomingRequest.Url.Host)}/oauth2_token", Type = SwaggerSecurityType.oauth2 } } }; AuthenticationContext.Current = bauth; } // Is there an options() method that can be called? List <KeyValuePair <String, Type> > resourceTypes = null; var optionsMethod = service.Behavior.Type.GetRuntimeMethod("Options", Type.EmptyTypes); ServiceOptions serviceOptions = null; if (optionsMethod != null) { var behaviorInstance = Activator.CreateInstance(service.Behavior.Type); serviceOptions = optionsMethod.Invoke(behaviorInstance, null) as ServiceOptions; if (serviceOptions != null) // Remove unused resources { resourceTypes = serviceOptions.Resources.Select(o => new KeyValuePair <string, Type>(o.ResourceName, o.ResourceType)).ToList(); } } else { // Resource types resourceTypes = service.Contracts.SelectMany(c => c.Type.GetCustomAttributes <ServiceKnownResourceAttribute>().Select(o => new KeyValuePair <String, Type>(o.Type.GetCustomAttribute <XmlRootAttribute>()?.ElementName, o.Type)).Where(o => !String.IsNullOrEmpty(o.Key))).ToList(); } // Create tags if (operations.Any(o => o.Rest.UriTemplate.Contains("{resourceType}"))) { this.Tags = resourceTypes.Select(o => new SwaggerTag(o.Key, MetadataComposerUtil.GetElementDocumentation(o.Value, MetaDataElementType.Summary))).ToList(); } // Process operations foreach (var operation in operations.GroupBy(o => o.Rest.UriTemplate.StartsWith("/") ? o.Rest.UriTemplate : "/" + o.Rest.UriTemplate)) { // If the path does not contain {resourceType} and there are no ServiceKnownTypes then proceed var path = new SwaggerPath(); foreach (var val in operation) { // Process operations var pathDefinition = new SwaggerPathDefinition(val.BehaviorMethod, val.ContractMethod); path.Add(val.Rest.Method.ToLower(), pathDefinition); if (pathDefinition.Consumes.Count == 0) { pathDefinition.Consumes.AddRange(this.Consumes); } if (pathDefinition.Produces.Count == 0) { pathDefinition.Produces.AddRange(this.Produces); } // Any faults? foreach (var flt in service.Contracts.SelectMany(t => t.Type.GetCustomAttributes <ServiceFaultAttribute>())) { pathDefinition.Responses.Add(flt.StatusCode, new SwaggerSchemaElement() { Description = flt.Condition, Schema = new SwaggerSchemaDefinition() { Reference = $"#/definitions/{MetadataComposerUtil.CreateSchemaReference(flt.FaultType)}", NetType = flt.FaultType } }); } } if (operation.Key.Contains("{resourceType}")) { foreach (var resource in resourceTypes) { var resourcePath = operation.Key.Replace("{resourceType}", resource.Key); if (this.Paths.ContainsKey(resourcePath) || resourcePath.Contains("history") && !typeof(IVersionedEntity).IsAssignableFrom(resource.Value)) { continue; } // Create a copy of the path and customize it to the resource List <String> unsupportedVerbs = new List <string>(); // Get the resource options for this resource ServiceResourceOptions resourceOptions = serviceOptions?.Resources.FirstOrDefault(o => o.ResourceName == resource.Key); var subPath = new SwaggerPath(path); foreach (var v in subPath) { // Check that this resource is supported var resourceCaps = resourceOptions?.Capabilities.FirstOrDefault(c => c.Capability == MetadataComposerUtil.VerbToCapability(v.Key, v.Value.Parameters.Count)); if (resourceOptions != null && resourceCaps == null) { unsupportedVerbs.Add(v.Key); continue; } // Add tags for resource v.Value.Tags.Add(resource.Key); v.Value.Parameters.RemoveAll(o => o.Name == "resourceType"); // Security? if (this.SecurityDefinitions.Count > 0 && resourceCaps?.Demand.Length > 0) { v.Value.Security = new List <SwaggerPathSecurity>() { new SwaggerPathSecurity() { { "oauth_user", resourceCaps.Demand.Distinct().ToList() } } }; } // Query parameters? if ((v.Key == "get" || v.Key == "head") && v.Value.Parameters.Count == 0) { // Build query parameters v.Value.Parameters = resource.Value.GetProperties(BindingFlags.Instance | BindingFlags.Public) .Where(o => o.GetCustomAttributes <XmlElementAttribute>().Any() || o.GetCustomAttribute <QueryParameterAttribute>() != null) .Select(o => new SwaggerParameter(o)) .ToList(); v.Value.Parameters.AddRange(new SwaggerParameter[] { new SwaggerParameter() { Name = "_offset", Type = SwaggerSchemaElementType.number, Description = "Offset of query results", Location = SwaggerParameterLocation.query }, new SwaggerParameter() { Name = "_count", Type = SwaggerSchemaElementType.number, Description = "Count of query results to include in result set", Location = SwaggerParameterLocation.query }, new SwaggerParameter() { Name = "_lean", Type = SwaggerSchemaElementType.boolean, Description = "When true, the server will only return minimal data by removing duplicates from the bundle's resource[] property. NOTE: This means that the .count parameter may not match the number of items in resource[], however you should continue to use the .count parameter to increase your offset", Location = SwaggerParameterLocation.query }, new SwaggerParameter() { Name = "_orderBy", Type = SwaggerSchemaElementType.@string, Description = "Indicates a series of parameters to order the result set by", Location = SwaggerParameterLocation.query }, new SwaggerParameter() { Name = "_queryId", Type = SwaggerSchemaElementType.@string, Description = "The unique identifier for the query (for continuation)", Location = SwaggerParameterLocation.query }, new SwaggerParameter() { Name = "_viewModel", Type = SwaggerSchemaElementType.@string, Description = "When using the view-model content-type, the view model to use", Location = SwaggerParameterLocation.query, Enum = new List <string>() { "min", "max" } } }); } // Replace the response if necessary var resourceSchemaRef = new SwaggerSchemaDefinition() { NetType = resource.Value, Reference = $"#/definitions/{MetadataComposerUtil.CreateSchemaReference(resource.Value)}" }; SwaggerSchemaElement schema = null; if (v.Value.Responses.TryGetValue(200, out schema)) { schema.Schema = resourceSchemaRef; } if (v.Value.Responses.TryGetValue(201, out schema)) { schema.Schema = resourceSchemaRef; } // Replace the body if necessary var bodyParm = v.Value.Parameters.FirstOrDefault(o => o.Location == SwaggerParameterLocation.body && o.Schema?.NetType?.IsAssignableFrom(resource.Value) == true); if (bodyParm != null) { bodyParm.Schema = resourceSchemaRef; } } // foreach subpath // Add the resource path? foreach (var nv in unsupportedVerbs) { subPath.Remove(nv); } this.Paths.Add(resourcePath, subPath); } } else { this.Paths.Add(operation.Key, path); } } // Now we want to add a definition for all references // This LINQ expression allows for scanning of any properties where there is currently no definition for a particular type var missingDefns = this.Paths.AsParallel().SelectMany( o => o.Value.SelectMany(p => p.Value?.Parameters.Select(r => r.Schema)) .Union(o.Value.SelectMany(p => p.Value?.Responses?.Values?.Select(r => r.Schema)))) .Union(this.Definitions.AsParallel().Where(o => o.Value.Properties != null).SelectMany(o => o.Value.Properties.Select(p => p.Value.Items ?? p.Value.Schema))) .Union(this.Definitions.AsParallel().Where(o => o.Value.AllOf != null).SelectMany(o => o.Value.AllOf)) .Select(s => s?.NetType) .Where(o => o != null && !this.Definitions.ContainsKey(MetadataComposerUtil.CreateSchemaReference(o))) .Distinct(); while (missingDefns.Count() > 0) { foreach (var def in missingDefns.AsParallel().ToList()) { var name = MetadataComposerUtil.CreateSchemaReference(def); if (!this.Definitions.ContainsKey(name)) { this.Definitions.Add(name, new SwaggerSchemaDefinition(def)); } } } // Create the definitions }