/// <summary> /// Initializes the type of the API. /// </summary> /// <param name="doneInterfaceTypes">The done interface types.</param> /// <param name="routes">The routes.</param> /// <param name="interfaceType">Type of the interface.</param> /// <param name="instance">The instance.</param> /// <param name="settings">The settings.</param> /// <param name="parentApiContractAttribute">The parent API class attribute.</param> /// <param name="parentApiModuleAttribute">The parent API module attribute.</param> /// <param name="omitApiTrackingAttribute">The omit API tracking attribute.</param> /// <param name="parentTokenRequiredAttribute">The parent token required attribute.</param> /// <exception cref="DataConflictException">routeKey</exception> private static void InitializeApiType(List <string> doneInterfaceTypes, Dictionary <ApiRouteIdentifier, RuntimeRoute> routes, Type interfaceType, object instance, RestApiSettings settings = null, ApiContractAttribute parentApiContractAttribute = null, ApiModuleAttribute parentApiModuleAttribute = null, OmitApiTrackingAttribute omitApiTrackingAttribute = null, TokenRequiredAttribute parentTokenRequiredAttribute = null) { if (routes != null && interfaceType != null && doneInterfaceTypes != null) { if (doneInterfaceTypes.Contains(interfaceType.FullName)) { return; } var apiContract = parentApiContractAttribute ?? interfaceType.GetCustomAttribute <ApiContractAttribute>(true); var omitApiTracking = omitApiTrackingAttribute ?? interfaceType.GetCustomAttribute <OmitApiTrackingAttribute>(true); var apiModule = parentApiModuleAttribute ?? interfaceType.GetCustomAttribute <ApiModuleAttribute>(true); var tokenRequiredAttribute = parentTokenRequiredAttribute ?? interfaceType.GetCustomAttribute <TokenRequiredAttribute>(true); var moduleName = apiModule?.ToString(); if (apiContract != null && !string.IsNullOrWhiteSpace(apiContract.Version)) { if (apiContract.Version.SafeEquals(ApiConstants.BuiltInFeatureVersionKeyword, StringComparison.OrdinalIgnoreCase)) { throw ExceptionFactory.CreateInvalidObjectException(nameof(apiContract.Version), reason: "<builtin> cannot be used as version due to it is used internally."); } foreach (var method in interfaceType.GetMethods()) { var apiOperationAttribute = method.GetCustomAttribute <ApiOperationAttribute>(true); #region Initialize based on ApiOperation if (apiOperationAttribute != null) { var permissions = new Dictionary <string, ApiPermissionAttribute>(); var additionalHeaderKeys = new HashSet <string>(); var apiPermissionAttributes = method.GetCustomAttributes <ApiPermissionAttribute>(true); var apiCacheAttribute = method.GetCustomAttribute <ApiCacheAttribute>(true); if (apiPermissionAttributes != null) { foreach (var one in apiPermissionAttributes) { permissions.Merge(one.PermissionIdentifier, one); } } var headerKeyAttributes = method.GetCustomAttributes <ApiHeaderAttribute>(true); if (headerKeyAttributes != null) { foreach (var one in headerKeyAttributes) { additionalHeaderKeys.Add(one.HeaderKey); } } var routeKey = ApiRouteIdentifier.FromApiObjects(apiContract, apiOperationAttribute); var tokenRequired = method.GetCustomAttribute <TokenRequiredAttribute>(true) ?? tokenRequiredAttribute; // If method can not support API cache, consider as no api cache. if (apiCacheAttribute != null && (!apiOperationAttribute.HttpMethod.Equals(HttpConstants.HttpMethod.Get, StringComparison.OrdinalIgnoreCase) || !apiCacheAttribute.InitializeParameterNames(method))) { apiCacheAttribute = null; } var runtimeRoute = new RuntimeRoute(routeKey, method, interfaceType, instance, !string.IsNullOrWhiteSpace(apiOperationAttribute.Action), tokenRequired != null && tokenRequired.TokenRequired, moduleName, apiOperationAttribute.ContentType, settings, apiCacheAttribute, omitApiTracking ?? method.GetCustomAttribute <OmitApiTrackingAttribute>(true), permissions, additionalHeaderKeys.ToList()); if (routes.ContainsKey(routeKey)) { throw new DataConflictException(nameof(routeKey), objectIdentity: routeKey?.ToString(), data: new { existed = routes[routeKey].SafeToString(), newMethod = method.GetFullName(), newInterface = interfaceType.FullName }); } // EntitySynchronizationModeAttribute var entitySynchronizationModeAttribute = method.GetCustomAttribute <EntitySynchronizationModeAttribute>(true); if (entitySynchronizationModeAttribute != null) { if (EntitySynchronizationModeAttribute.IsReturnTypeMatched(method.ReturnType)) { runtimeRoute.OperationParameters.EntitySynchronizationMode = entitySynchronizationModeAttribute; } } routes.Add(routeKey, runtimeRoute); } #endregion Initialize based on ApiOperation } foreach (var one in interfaceType.GetInterfaces()) { InitializeApiType(doneInterfaceTypes, routes, one, instance, settings, apiContract, apiModule, omitApiTracking, tokenRequiredAttribute); } //Special NOTE: // Move this add action in scope of if apiContract is valid. // Reason: in complicated cases, when [A:Interface1] without ApiContract, but [Interface2: Interface] with defining ApiContract, and [B: A, Interface2], then correct contract definition might be missed. doneInterfaceTypes.Add(interfaceType.FullName); } } }
/// <summary> /// Writes the API HTML document. /// </summary> /// <param name="builder">The builder.</param> /// <param name="apiServiceType">Type of the API service.</param> /// <param name="apiContractOptions">The API contract options.</param> /// <param name="classTokenRequiredAttribute">The class token required attribute.</param> /// <param name="enumSets">The enum sets.</param> protected void WriteApiHtmlDocument(StringBuilder builder, Type apiServiceType, IApiContractOptions apiContractOptions, TokenRequiredAttribute classTokenRequiredAttribute, HashSet <Type> enumSets) { if (builder != null && apiServiceType != null && apiContractOptions != null) { foreach (MethodInfo one in apiServiceType.GetMethodInfoWithinAttribute <ApiOperationAttribute>(true, BindingFlags.Instance | BindingFlags.Public)) { // Considering in interface, can NOT tell is async or not, check return type is Task or Task<T>. bool isAsync = one.IsAsync() || one.ReturnType.IsTask(); var apiOperationAttribute = one.GetCustomAttribute <ApiOperationAttribute>(true); if (apiOperationAttribute != null) { StringBuilder bodyBuilder = new StringBuilder(4096); #region Entity Synchronization Status var entitySynchronizationAttribute = one.GetCustomAttribute <EntitySynchronizationModeAttribute>(true); if (entitySynchronizationAttribute != null && !EntitySynchronizationModeAttribute.IsReturnTypeMatched(one.ReturnType)) { entitySynchronizationAttribute = null; } #endregion Entity Synchronization Status //Original declaration bodyBuilder.Append("<h3>.NET Declaration</h3>"); bodyBuilder.AppendFormat(isAsync ? "<div><span style=\"color:red;font-weight:bold;\" title=\"Async\">[A] </span> {0}</div>" : "<div>{0}</div>", one.ToDeclarationCodeLook().ToHtmlEncodedText()); bodyBuilder.Append("<hr />"); //Try append description var apiDescriptionAttributes = one.GetCustomAttributes <ApiDescriptionAttribute>(true); if (apiDescriptionAttributes != null && apiDescriptionAttributes.Any()) { foreach (var description in apiDescriptionAttributes) { if (!string.IsNullOrWhiteSpace(description.Description)) { bodyBuilder.AppendFormat("<div>{0}</div>", description.Description.ToHtmlEncodedText()); } } } // Customized headers var apiCustomizedHeaderAttributes = one.GetCustomAttributes <ApiHeaderAttribute>(true); if (apiCustomizedHeaderAttributes.HasItem()) { bodyBuilder.Append("<h3>Customized headers</h3><hr />"); bodyBuilder.Append("<ul>"); foreach (var item in apiCustomizedHeaderAttributes) { bodyBuilder.AppendFormat(customHeaderFormat, item.HeaderKey); } bodyBuilder.Append("</ul>"); } var obsolete = one.GetCustomAttribute <ObsoleteAttribute>(true); if (obsolete != null) { bodyBuilder.AppendFormat("<div style=\"color:red;\"> Obsoleted: {0}</div>", obsolete.Message.ToHtmlEncodedText()); } bodyBuilder.Append("<div>Following sample shows how to use via REST API.</div>"); #region Request bodyBuilder.Append("<h3>Request</h3><hr />"); bodyBuilder.Append("<url>"); if (string.IsNullOrWhiteSpace(apiContractOptions.Realm)) { bodyBuilder.AppendFormat("{0} /api/{1}/{2}/", apiOperationAttribute.HttpMethod, apiContractOptions.Version, apiOperationAttribute.ResourceName); } else { bodyBuilder.AppendFormat("{0} /{1}/api/{2}/{3}/", apiContractOptions.Realm, apiOperationAttribute.HttpMethod, apiContractOptions.Version, apiOperationAttribute.ResourceName); } if (!string.IsNullOrWhiteSpace(apiOperationAttribute.Action)) { bodyBuilder.AppendFormat("{0}/", apiOperationAttribute.Action); } var parameters = one.GetParameters(); var parameterIsHandled = false; if (parameters.Length == 0) { parameterIsHandled = true; } else if (parameters.Length == 1 && (apiOperationAttribute.HttpMethod.Equals(HttpConstants.HttpMethod.Get, StringComparison.OrdinalIgnoreCase) || apiOperationAttribute.HttpMethod.Equals(HttpConstants.HttpMethod.Delete, StringComparison.OrdinalIgnoreCase))) { if (parameters[0].ParameterType == typeof(string) || parameters[0].ParameterType.IsValueType) { bodyBuilder.AppendFormat("<span style=\"font-style:italic; font-weight:bold;color:#CC0000;\" title=\"Sample value for {0}\">", parameters[0].Name); FillSampleValue(bodyBuilder, parameters[0].ParameterType, enumSets, 0, fieldName: parameters[0].Name, ignoreQuote: true); bodyBuilder.Append("</span>"); parameterIsHandled = true; } } else if (parameters.Length > 1 && (apiOperationAttribute.HttpMethod.Equals(HttpConstants.HttpMethod.Get, StringComparison.OrdinalIgnoreCase) || apiOperationAttribute.HttpMethod.Equals(HttpConstants.HttpMethod.Delete, StringComparison.OrdinalIgnoreCase))) { bodyBuilder.Append("?"); foreach (var parameterItem in parameters) { bodyBuilder.AppendFormat("{0}=", parameterItem.Name); FillSampleValue(bodyBuilder, parameterItem.ParameterType, enumSets, 0, parameterItem.Name, true, true); bodyBuilder.Append("&"); } bodyBuilder.RemoveLastIfMatch('&', true); parameterIsHandled = true; } bodyBuilder.Append("</url>"); var currentTokenRequiredAttribute = one.GetCustomAttribute <TokenRequiredAttribute>(true) ?? classTokenRequiredAttribute; if (currentTokenRequiredAttribute != null && currentTokenRequiredAttribute.TokenRequired) { bodyBuilder.AppendLineWithFormat(requestHeaderFormat, TokenKey, "[YourTokenValue]"); } if (entitySynchronizationAttribute != null) { bodyBuilder.AppendLineWithFormat(requestHeaderFormat, entitySynchronizationAttribute.IfModifiedSinceKey, DateTime.UtcNow.AddDays(-2).ToFullDateTimeString()); } bodyBuilder.Append("<pre class=\"CodeContainer\" style=\"font-family: monospace;font-size:14px;\">"); if (!parameterIsHandled) { if (parameters.Length == 1) { FillSampleValue(bodyBuilder, parameters[0].ParameterType, enumSets, 0, fieldName: parameters[0].Name, followingProperty: false); } else { builder.AppendFormat(objectBrace, "{"); foreach (var parameterItem in parameters) { FillProperty(bodyBuilder, parameterItem.Name, parameterItem.ParameterType); AppendColon(bodyBuilder); FillSampleValue(bodyBuilder, parameterItem.ParameterType, enumSets, 1, followingProperty: true); AppendComma(bodyBuilder); } RemoveUnnecessaryColon(bodyBuilder); bodyBuilder.AppendFormat(objectBrace, "}"); } } bodyBuilder.Append("</pre>"); #endregion Request #region Response bodyBuilder.Append("<h3>Response</h3><hr />"); bodyBuilder.AppendLineWithFormat(requestHeaderFormat, HttpConstants.HttpHeader.ContentType, apiOperationAttribute.ContentType.SafeToString(HttpConstants.ContentType.Json)); if (entitySynchronizationAttribute != null) { bodyBuilder.AppendLineWithFormat(requestHeaderFormat, entitySynchronizationAttribute.LastModifiedKey, DateTime.UtcNow.ToFullDateTimeString()); } bodyBuilder.Append("<pre class=\"CodeContainer\" style=\"font-family: monospace;font-size:14px;\">"); var returnType = one.ReturnType; if (isAsync) { returnType = returnType.GetTaskUnderlyingType() ?? returnType; } if (returnType.IsVoid() ?? true) { bodyBuilder.Append("<span style=\"font-style:italic; font-weight: bold; color: #999999;\">void</span>"); } else { FillSampleValue(bodyBuilder, returnType, enumSets, 0); } bodyBuilder.Append("</pre>"); #endregion Response #region Http Status // Http Status bodyBuilder.Append("<h3>Http Status & Exceptions</h3><hr />"); bodyBuilder.Append("<ul>"); if (one.ReturnType.IsVoid() ?? false) { bodyBuilder.AppendFormat(httpStatusFormat, (int)HttpStatusCode.NoContent, HttpStatusCode.NoContent.ToString(), "If no error or exception.", "green"); } else { bodyBuilder.AppendFormat(httpStatusFormat, (int)HttpStatusCode.OK, HttpStatusCode.OK.ToString(), "If no error or exception.", "green"); if (entitySynchronizationAttribute != null) { bodyBuilder.AppendFormat(httpStatusFormat, (int)HttpStatusCode.NotModified, HttpStatusCode.NotModified.ToString(), "If no modified since specific time stamp", "green"); } } bodyBuilder.AppendFormat(httpStatusFormat, (int)HttpStatusCode.BadRequest, HttpStatusCode.BadRequest.ToString(), "If input value or/and format is invalid.", "red"); if (currentTokenRequiredAttribute != null && currentTokenRequiredAttribute.TokenRequired) { bodyBuilder.AppendFormat(httpStatusFormat, (int)HttpStatusCode.Unauthorized, HttpStatusCode.Unauthorized.ToString(), "If token is invalid or not given.", "orange"); } bodyBuilder.AppendFormat(httpStatusFormat, (int)HttpStatusCode.Forbidden, HttpStatusCode.Forbidden.ToString(), "If action is forbidden.", "orange"); bodyBuilder.AppendFormat(httpStatusFormat, (int)HttpStatusCode.NotFound, HttpStatusCode.NotFound.ToString(), "If resource is not found.", "orange"); bodyBuilder.AppendFormat(httpStatusFormat, (int)HttpStatusCode.Conflict, HttpStatusCode.Conflict.ToString(), "If data conflicts.", "orange"); bodyBuilder.AppendFormat(httpStatusFormat, (int)HttpStatusCode.InternalServerError, HttpStatusCode.InternalServerError.ToString(), "If server feature is not working or has defect(s).", "red"); bodyBuilder.AppendFormat(httpStatusFormat, (int)HttpStatusCode.NotImplemented, HttpStatusCode.NotImplemented.ToString(), "If server feature is not implemented yet.", "red"); bodyBuilder.Append("</ul>"); #endregion Http Status builder.AppendFormat(panel, one.Name, bodyBuilder.ToString(), one.Name, "#"); } } } }