pass token to nswag apiclient in .Netcore - asp.net-core

I have generated my APIclient Code using Nswagstudio as you can see :
//----------------------
// <auto-generated>
// Generated using the NSwag toolchain v13.6.1.0 (NJsonSchema v10.1.21.0 (Newtonsoft.Json v12.0.0.0)) (http://NSwag.org)
// </auto-generated>
//----------------------
#pragma warning disable 108 // Disable "CS0108 '{derivedDto}.ToJson()' hides inherited member '{dtoBase}.ToJson()'. Use the new keyword if hiding was intended."
#pragma warning disable 114 // Disable "CS0114 '{derivedDto}.RaisePropertyChanged(String)' hides inherited member 'dtoBase.RaisePropertyChanged(String)'. To make the current member override that implementation, add the override keyword. Otherwise add the new keyword."
#pragma warning disable 472 // Disable "CS0472 The result of the expression is always 'false' since a value of type 'Int32' is never equal to 'null' of type 'Int32?'
#pragma warning disable 1573 // Disable "CS1573 Parameter '...' has no matching param tag in the XML comment for ...
#pragma warning disable 1591 // Disable "CS1591 Missing XML comment for publicly visible type or member ..."
namespace MyNamespace
{
using System = global::System;
[System.CodeDom.Compiler.GeneratedCode("NSwag", "13.6.1.0 (NJsonSchema v10.1.21.0 (Newtonsoft.Json v12.0.0.0))")]
public partial interface IDefault1Client
{
/// <returns>Success</returns>
/// <exception cref="ApiException">A server side error occurred.</exception>
System.Threading.Tasks.Task<Jwttoken> CreateTokenAsync();
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
/// <returns>Success</returns>
/// <exception cref="ApiException">A server side error occurred.</exception>
System.Threading.Tasks.Task<Jwttoken> CreateTokenAsync(System.Threading.CancellationToken cancellationToken);
/// <returns>Success</returns>
/// <exception cref="ApiException">A server side error occurred.</exception>
System.Threading.Tasks.Task<System.Collections.Generic.ICollection<string>> ReturnListAsync();
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
/// <returns>Success</returns>
/// <exception cref="ApiException">A server side error occurred.</exception>
System.Threading.Tasks.Task<System.Collections.Generic.ICollection<string>> ReturnListAsync(System.Threading.CancellationToken cancellationToken);
}
[System.CodeDom.Compiler.GeneratedCode("NSwag", "13.6.1.0 (NJsonSchema v10.1.21.0 (Newtonsoft.Json v12.0.0.0))")]
public partial class Default1Client : IDefault1Client
{
private string _baseUrl = "";
private System.Lazy<Newtonsoft.Json.JsonSerializerSettings> _settings;
public Default1Client(string baseUrl)
{
BaseUrl = baseUrl;
_settings = new System.Lazy<Newtonsoft.Json.JsonSerializerSettings>(CreateSerializerSettings);
}
private Newtonsoft.Json.JsonSerializerSettings CreateSerializerSettings()
{
var settings = new Newtonsoft.Json.JsonSerializerSettings();
UpdateJsonSerializerSettings(settings);
return settings;
}
public string BaseUrl
{
get { return _baseUrl; }
set { _baseUrl = value; }
}
protected Newtonsoft.Json.JsonSerializerSettings JsonSerializerSettings { get { return _settings.Value; } }
partial void UpdateJsonSerializerSettings(Newtonsoft.Json.JsonSerializerSettings settings);
partial void PrepareRequest(System.Net.Http.HttpClient client, System.Net.Http.HttpRequestMessage request, string url);
partial void PrepareRequest(System.Net.Http.HttpClient client, System.Net.Http.HttpRequestMessage request, System.Text.StringBuilder urlBuilder);
partial void ProcessResponse(System.Net.Http.HttpClient client, System.Net.Http.HttpResponseMessage response);
/// <returns>Success</returns>
/// <exception cref="ApiException">A server side error occurred.</exception>
public System.Threading.Tasks.Task<Jwttoken> CreateTokenAsync()
{
return CreateTokenAsync(System.Threading.CancellationToken.None);
}
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
/// <returns>Success</returns>
/// <exception cref="ApiException">A server side error occurred.</exception>
public async System.Threading.Tasks.Task<Jwttoken> CreateTokenAsync(System.Threading.CancellationToken cancellationToken)
{
var urlBuilder_ = new System.Text.StringBuilder();
urlBuilder_.Append(BaseUrl != null ? BaseUrl.TrimEnd('/') : "").Append("/api/Default1/CreateToken");
var client_ = await CreateHttpClientAsync(cancellationToken).ConfigureAwait(false);
try
{
using (var request_ = new System.Net.Http.HttpRequestMessage())
{
request_.Method = new System.Net.Http.HttpMethod("GET");
request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("text/plain"));
PrepareRequest(client_, request_, urlBuilder_);
var url_ = urlBuilder_.ToString();
request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute);
PrepareRequest(client_, request_, url_);
var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
try
{
var headers_ = System.Linq.Enumerable.ToDictionary(response_.Headers, h_ => h_.Key, h_ => h_.Value);
if (response_.Content != null && response_.Content.Headers != null)
{
foreach (var item_ in response_.Content.Headers)
headers_[item_.Key] = item_.Value;
}
ProcessResponse(client_, response_);
var status_ = ((int)response_.StatusCode).ToString();
if (status_ == "200")
{
var objectResponse_ = await ReadObjectResponseAsync<Jwttoken>(response_, headers_).ConfigureAwait(false);
return objectResponse_.Object;
}
else
if (status_ != "200" && status_ != "204")
{
var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false);
throw new ApiException("The HTTP status code of the response was not expected (" + (int)response_.StatusCode + ").", (int)response_.StatusCode, responseData_, headers_, null);
}
return default(Jwttoken);
}
finally
{
if (response_ != null)
response_.Dispose();
}
}
}
finally
{
if (client_ != null)
client_.Dispose();
}
}
/// <returns>Success</returns>
/// <exception cref="ApiException">A server side error occurred.</exception>
public System.Threading.Tasks.Task<System.Collections.Generic.ICollection<string>> ReturnListAsync()
{
return ReturnListAsync(System.Threading.CancellationToken.None);
}
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
/// <returns>Success</returns>
/// <exception cref="ApiException">A server side error occurred.</exception>
public async System.Threading.Tasks.Task<System.Collections.Generic.ICollection<string>> ReturnListAsync(System.Threading.CancellationToken cancellationToken)
{
var urlBuilder_ = new System.Text.StringBuilder();
urlBuilder_.Append(BaseUrl != null ? BaseUrl.TrimEnd('/') : "").Append("/api/Default1/ReturnList");
var client_ = await CreateHttpClientAsync(cancellationToken).ConfigureAwait(false);
try
{
using (var request_ = new System.Net.Http.HttpRequestMessage())
{
request_.Method = new System.Net.Http.HttpMethod("GET");
request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("text/plain"));
PrepareRequest(client_, request_, urlBuilder_);
var url_ = urlBuilder_.ToString();
request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute);
PrepareRequest(client_, request_, url_);
var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
try
{
var headers_ = System.Linq.Enumerable.ToDictionary(response_.Headers, h_ => h_.Key, h_ => h_.Value);
if (response_.Content != null && response_.Content.Headers != null)
{
foreach (var item_ in response_.Content.Headers)
headers_[item_.Key] = item_.Value;
}
ProcessResponse(client_, response_);
var status_ = ((int)response_.StatusCode).ToString();
if (status_ == "200")
{
var objectResponse_ = await ReadObjectResponseAsync<System.Collections.Generic.ICollection<string>>(response_, headers_).ConfigureAwait(false);
return objectResponse_.Object;
}
else
if (status_ != "200" && status_ != "204")
{
var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false);
throw new ApiException("The HTTP status code of the response was not expected (" + (int)response_.StatusCode + ").", (int)response_.StatusCode, responseData_, headers_, null);
}
return default(System.Collections.Generic.ICollection<string>);
}
finally
{
if (response_ != null)
response_.Dispose();
}
}
}
finally
{
if (client_ != null)
client_.Dispose();
}
}
protected struct ObjectResponseResult<T>
{
public ObjectResponseResult(T responseObject, string responseText)
{
this.Object = responseObject;
this.Text = responseText;
}
public T Object { get; }
public string Text { get; }
}
public bool ReadResponseAsString { get; set; }
protected virtual async System.Threading.Tasks.Task<ObjectResponseResult<T>> ReadObjectResponseAsync<T>(System.Net.Http.HttpResponseMessage response, System.Collections.Generic.IReadOnlyDictionary<string, System.Collections.Generic.IEnumerable<string>> headers)
{
if (response == null || response.Content == null)
{
return new ObjectResponseResult<T>(default(T), string.Empty);
}
if (ReadResponseAsString)
{
var responseText = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
try
{
var typedBody = Newtonsoft.Json.JsonConvert.DeserializeObject<T>(responseText, JsonSerializerSettings);
return new ObjectResponseResult<T>(typedBody, responseText);
}
catch (Newtonsoft.Json.JsonException exception)
{
var message = "Could not deserialize the response body string as " + typeof(T).FullName + ".";
throw new ApiException(message, (int)response.StatusCode, responseText, headers, exception);
}
}
else
{
try
{
using (var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
using (var streamReader = new System.IO.StreamReader(responseStream))
using (var jsonTextReader = new Newtonsoft.Json.JsonTextReader(streamReader))
{
var serializer = Newtonsoft.Json.JsonSerializer.Create(JsonSerializerSettings);
var typedBody = serializer.Deserialize<T>(jsonTextReader);
return new ObjectResponseResult<T>(typedBody, string.Empty);
}
}
catch (Newtonsoft.Json.JsonException exception)
{
var message = "Could not deserialize the response body stream as " + typeof(T).FullName + ".";
throw new ApiException(message, (int)response.StatusCode, string.Empty, headers, exception);
}
}
}
private string ConvertToString(object value, System.Globalization.CultureInfo cultureInfo)
{
if (value is System.Enum)
{
string name = System.Enum.GetName(value.GetType(), value);
if (name != null)
{
var field = System.Reflection.IntrospectionExtensions.GetTypeInfo(value.GetType()).GetDeclaredField(name);
if (field != null)
{
var attribute = System.Reflection.CustomAttributeExtensions.GetCustomAttribute(field, typeof(System.Runtime.Serialization.EnumMemberAttribute))
as System.Runtime.Serialization.EnumMemberAttribute;
if (attribute != null)
{
return attribute.Value != null ? attribute.Value : name;
}
}
return System.Convert.ToString(System.Convert.ChangeType(value, System.Enum.GetUnderlyingType(value.GetType()), cultureInfo));
}
}
else if (value is bool)
{
return System.Convert.ToString(value, cultureInfo).ToLowerInvariant();
}
else if (value is byte[])
{
return System.Convert.ToBase64String((byte[]) value);
}
else if (value != null && value.GetType().IsArray)
{
var array = System.Linq.Enumerable.OfType<object>((System.Array) value);
return string.Join(",", System.Linq.Enumerable.Select(array, o => ConvertToString(o, cultureInfo)));
}
return System.Convert.ToString(value, cultureInfo);
}
}
[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.1.21.0 (Newtonsoft.Json v12.0.0.0)")]
public partial class Jwttoken
{
[Newtonsoft.Json.JsonProperty("token", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
public string Token { get; set; }
}
[System.CodeDom.Compiler.GeneratedCode("NSwag", "13.6.1.0 (NJsonSchema v10.1.21.0 (Newtonsoft.Json v12.0.0.0))")]
public partial class ApiException : System.Exception
{
public int StatusCode { get; private set; }
public string Response { get; private set; }
public System.Collections.Generic.IReadOnlyDictionary<string, System.Collections.Generic.IEnumerable<string>> Headers { get; private set; }
public ApiException(string message, int statusCode, string response, System.Collections.Generic.IReadOnlyDictionary<string, System.Collections.Generic.IEnumerable<string>> headers, System.Exception innerException)
: base(message + "\n\nStatus: " + statusCode + "\nResponse: \n" + response.Substring(0, response.Length >= 512 ? 512 : response.Length), innerException)
{
StatusCode = statusCode;
Response = response;
Headers = headers;
}
public override string ToString()
{
return string.Format("HTTP Response: \n\n{0}\n\n{1}", Response, base.ToString());
}
}
[System.CodeDom.Compiler.GeneratedCode("NSwag", "13.6.1.0 (NJsonSchema v10.1.21.0 (Newtonsoft.Json v12.0.0.0))")]
public partial class ApiException<TResult> : ApiException
{
public TResult Result { get; private set; }
public ApiException(string message, int statusCode, string response, System.Collections.Generic.IReadOnlyDictionary<string, System.Collections.Generic.IEnumerable<string>> headers, TResult result, System.Exception innerException)
: base(message, statusCode, response, headers, innerException)
{
Result = result;
}
}
}
So I have added this code to my MVC core endpoint with these configurations
services.AddScoped<IDefault1Client>(provider =>
{
return new Default1Client("https://localhost:44381");
});
My apli using JWT as a security pattern.But myproblem is how can I pass my token into httpclient of the above code thatis generated by nswag studio .?

If i correct understand you
You have partial method
PrepareRequest
You can write header adding in this method
UPD:
partial void PrepareRequest(IHttpClient client, System.Net.Http.HttpRequestMessage request, string url)
{
var (name, value) = GetSecurityHeader();
request.Headers.Add(name, value);
}

Related

How to avoid (or handle) non-existing endpoints in a custom AuthorizationHandler in .net Core 3 Api

I have created my custom AuthorizationHandler.
public class VdsAuthorizationHandler : IAuthorizationHandler
{
private readonly IConfiguration _configuration;
private readonly IHttpContextAccessor _httpContext;
private readonly IHttpClientFactory _clientFactory;
public VdsAuthorizationHandler(IConfiguration configuration, IHttpContextAccessor httpContextAccessor, IHttpClientFactory clientFactory)
{
_configuration = configuration;
_httpContext = httpContextAccessor;
_clientFactory = clientFactory;
}
public Task HandleAsync(AuthorizationHandlerContext context)
{
UserRightsDefinition appUserRights = null;
var authRequirement = context.PendingRequirements.Where(x => x is DenyAnonymousAuthorizationRequirement).FirstOrDefault();
if (authRequirement != null)
{
if(_httpContext.HttpContext.User.Identity.IsAuthenticated)
{
context.Succeed(authRequirement);
} else
{
context.Fail();
return Task.CompletedTask;
}
}
var skipRequirement = context.PendingRequirements.Where(x => x is VdsSkipRequirement).FirstOrDefault();
if (skipRequirement != null)
{
context.Succeed(skipRequirement);
}
else
{
var req = _clientFactory.CreateClient("Auth");
var reqMessage = new HttpRequestMessage(HttpMethod.Get, "Auth/userrights");
reqMessage.Content = new StringContent("", Encoding.UTF8, "application/json");
var response = req.SendAsync(reqMessage);
if (response.Result.IsSuccessStatusCode)
{
var userRights = JsonConvert.DeserializeObject<Dictionary<string, UserRightsDefinition>>(response.Result.Content.ReadAsStringAsync().Result);
userRights.TryGetValue(_configuration.GetValue<string>("AppSettings:AppCode"), out appUserRights);
}
else
{
throw new Vds404NotFoundException();
}
}
if (appUserRights != null)
{
var pendingRequirements = context.PendingRequirements.ToList();
foreach (var requirement in pendingRequirements)
{
if (requirement is VdsPermissionsRequirement)
{
if (((VdsPermissionsRequirement)requirement).Permissions.Intersect(appUserRights.Permissions).Any())
{
context.Succeed(requirement);
}
}
else if (requirement is VdsGroupsRequirement)
{
if (((VdsGroupsRequirement)requirement).Groups.Intersect(appUserRights.Groups).Any())
{
context.Succeed(requirement);
}
}
}
}
return Task.CompletedTask;
}
}
}
It does what it has to do, except in one case : If I access a non-existing endpoint (ex. /api/fakeendpoint), HandleAsync is still called. So I need to be able to do one of these 2 things :
Either evaluate if the requested endpoint is valid or;
Avoid calling HandleAsync if the endpoint is invalid
How can I do that?
EDIT:
I might have found an answer, if anyone can confirm :
if(_httpContext.HttpContext.GetRouteData().Values.Count == 0)
{
context.Fail();
return Task.CompletedTask;
}

Custom validator for input parameter

I currently have this controller
/// <summary>
/// API methods for working with test
/// </summary>
[RoutePrefix("api/terminal")]
public class TerminalController : ApiController
{
[HttpGet]
[Route("{terminalId}/validation")]
public IHttpActionResult ValidateTerminal([MinUnsignValue(10)] long terminalId)
{
return Ok();
}
}
And custom validator for input parameter
public class MinUnsignValueAttribute : ValidationAttribute
{
private readonly ulong _minValue;
public MinUnsignValueAttribute(ulong minValue)
{
_minValue = minValue;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
//if value < _minValue return new ValidationResult("false")
return ValidationResult.Success;
}
}
When I send 6 the validator ignored and action invoked. It`s caused because web api pipeline consist of IActionFilter[] IAuthenticationFilter[] IAuthorizationFilter[] authorizationFilters IExceptionFilter[].
Is there a way to integrate my custom attribute to the pipeline?
ok, I didn't found an answer. wrote it myself
public class ParametersFilter : ActionFilterAttribute
{
class Wrapper
{
public Wrapper(MethodInfo methodInfo, object validationAttributeInstance)
{
MethodInfo = methodInfo;
ValidationAttributeInstance = validationAttributeInstance;
}
public MethodInfo MethodInfo { get; }
public object ValidationAttributeInstance { get; }
}
static readonly ConcurrentDictionary<string, Wrapper> Cache = new ConcurrentDictionary<string, Wrapper>();
static readonly HashSet<string> Registry = new HashSet<string>();
public override void OnActionExecuting(HttpActionContext actionContext)
{
if (!Registry.Contains(actionContext.ActionDescriptor.ActionName))
{
Registry.Add(actionContext.ActionDescriptor.ActionName);
//first invokation, have to check
Type controllerType = actionContext.ControllerContext.Controller.GetType();
foreach (MethodInfo methodInfo in controllerType.GetMethods())
{
if (actionContext.ActionDescriptor.ActionName != methodInfo.Name) continue;
ParameterInfo[] parameters = methodInfo.GetParameters();
if (parameters.Length != actionContext.ActionArguments.Count) continue;
foreach (ParameterInfo parameterInfo in parameters)
{
//The primitive types are Boolean, Byte, SByte, Int16, UInt16, Int32, UInt32, Int64, UInt64, IntPtr, UIntPtr, Char, Double, and Single.
if (!parameterInfo.ParameterType.IsPrimitive) continue;
if (!actionContext.ActionArguments.ContainsKey(parameterInfo.Name)) continue;
var customAttributesData = parameterInfo.GetCustomAttributesData();
foreach (CustomAttributeData customAttributeData in customAttributesData)
{
if (!customAttributeData.AttributeType.IsSubclassOf(typeof(ValidationAttribute))) continue;
var validationAttributeInstance = customAttributeData.Constructor.Invoke(customAttributeData.ConstructorArguments.Select(x => x.Value).ToArray()) as ValidationAttribute;
if (validationAttributeInstance == null) continue;
MethodInfo method = validationAttributeInstance
.GetType()
.GetMethod("IsValid", BindingFlags.NonPublic | BindingFlags.Instance);
Cache.TryAdd(
$"{actionContext.ActionDescriptor.ActionName}{parameterInfo.Name}",
new Wrapper(method, validationAttributeInstance));
}
}
}
}
foreach (var actionArgument in actionContext.ActionArguments)
{
Wrapper wrapper;
if (!Cache.TryGetValue(
$"{actionContext.ActionDescriptor.ActionName}{actionArgument.Key}", out wrapper))
{
continue;
}
object instance = actionArgument.Value;
var context = new ValidationContext(instance)
{
DisplayName = actionArgument.Key,
MemberName = actionArgument.Key
};
var results = wrapper.MethodInfo.Invoke(wrapper.ValidationAttributeInstance, new[] { instance, context }) as ValidationResult;
if (results != ValidationResult.Success)
{
var response = new Utils.WebApi.Common.Response.WebResponse
{
IsSuccess = false,
ErrorMessage = results?.ErrorMessage
};
actionContext.Response = new HttpResponseMessage
{
StatusCode = HttpStatusCode.BadRequest,
Content = new ObjectContent(
response.GetType(),
response,
new JsonMediaTypeFormatter())
};
break;
}
}
base.OnActionExecuting(actionContext);
}
}

Configure cors to allow all subdomains using ASP.NET Core (Asp.net 5, MVC6, VNext)

I have cors setup correctly in an ASP.NET Core web app. Im using the following package...
"Microsoft.AspNet.Cors": "6.0.0-rc1-final"
and here is the startup.cs snippet...
public virtual IServiceProvider ConfigureServices(IServiceCollection services)
{
services.AddCors
(
options =>
{
options.AddPolicy
(
CORSDefaults.PolicyName,
builder =>
{
//From config...
var allowedDomains = new []{"http://aaa.somewhere.com","https://aaa.somewhere.com","http://bbb.somewhere.com","https://bbb.somewhere.com"};
//Load it
builder
.WithOrigins(allowedDomains)
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials();
}
);
}
);
}
This works great except that the list of subdomains to allow is growing fast and I want to allow all subdomains of "somewhere.com". Something like "*.somewhere.com". I cant seem to find any documentation on how to do this in the new ASP.NET Core (MVC6, ASP.NET5, VNext). All the docs/examples I'm finding that demonstrate how to do this are for earlier versions of MVC or WebApi. How can I achieve this in the new stack?
This has now been implemented in version 2.0.0. In your ConfigureServices use the following:
options.AddPolicy("MyCorsPolicy",
builder => builder
.SetIsOriginAllowedToAllowWildcardSubdomains()
.WithOrigins("https://*.mydomain.com")
.AllowAnyMethod()
.AllowCredentials()
.AllowAnyHeader()
.Build()
);
Also, don't forget to call UseCors in your Configure call too:
app.UseCors("MyCorsPolicy");
I submitted a pull request to the ASP.NET team with this change so hopefully it will make it into the nuget package. Until then, I use this workaround.
Below you register cors as usual with the exception of having to register the WildCardCorsService class in the di container.
public virtual IServiceProvider ConfigureServices(IServiceCollection services)
{
services.TryAdd(ServiceDescriptor.Transient<ICorsService, WildCardCorsService>());
services.AddCors
(
options =>
{
options.AddPolicy
(
CORSDefaults.PolicyName,
builder =>
{
builder
.WithOrigins("http://*.withwildcardsubdomain.com", "http://nowildcard.com")
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials();
}
);
}
);
}
Save this class locally in your solution. It is a copy and edit of the Microsoft.AspNet.Cors.CorsService.cs class to allow it to handle wildcard subdomains. If it finds a wildcard char '*' it will check if the root domain matches on allowed origins and actual origin. It does NOT support partial wildcard matching.
namespace Microsoft.AspNet.Cors.Infrastructure
{
/// <summary>
/// This ICorsService should be used in place of the official default CorsService to support origins
/// like http://*.example.comwhich will allow any subdomain for example.com
/// </summary>
public class WildCardCorsService : ICorsService
{
private readonly CorsOptions _options;
/// <summary>
/// Creates a new instance of the <see cref="CorsService"/>.
/// </summary>
/// <param name="options">The option model representing <see cref="CorsOptions"/>.</param>
public WildCardCorsService(IOptions<CorsOptions> options)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
_options = options.Value;
}
/// <summary>
/// Looks up a policy using the <paramref name="policyName"/> and then evaluates the policy using the passed in
/// <paramref name="context"/>.
/// </summary>
/// <param name="requestContext"></param>
/// <param name="policyName"></param>
/// <returns>A <see cref="CorsResult"/> which contains the result of policy evaluation and can be
/// used by the caller to set appropriate response headers.</returns>
public CorsResult EvaluatePolicy(HttpContext context, string policyName)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
var policy = _options.GetPolicy(policyName);
return EvaluatePolicy(context, policy);
}
/// <inheritdoc />
public CorsResult EvaluatePolicy(HttpContext context, CorsPolicy policy)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (policy == null)
{
throw new ArgumentNullException(nameof(policy));
}
var corsResult = new CorsResult();
var accessControlRequestMethod = context.Request.Headers[Microsoft.AspNet.Cors.Infrastructure.CorsConstants.AccessControlRequestMethod];
if (string.Equals(context.Request.Method, Microsoft.AspNet.Cors.Infrastructure.CorsConstants.PreflightHttpMethod, StringComparison.Ordinal) &&
!StringValues.IsNullOrEmpty(accessControlRequestMethod))
{
EvaluatePreflightRequest(context, policy, corsResult);
}
else
{
EvaluateRequest(context, policy, corsResult);
}
return corsResult;
}
public virtual void EvaluateRequest(HttpContext context, CorsPolicy policy, CorsResult result)
{
var origin = context.Request.Headers[Microsoft.AspNet.Cors.Infrastructure.CorsConstants.Origin];
if (!OriginIsAllowed(origin, policy))
{
return;
}
AddOriginToResult(origin, policy, result);
result.SupportsCredentials = policy.SupportsCredentials;
AddHeaderValues(result.AllowedExposedHeaders, policy.ExposedHeaders);
}
public virtual void EvaluatePreflightRequest(HttpContext context, CorsPolicy policy, CorsResult result)
{
var origin = context.Request.Headers[Microsoft.AspNet.Cors.Infrastructure.CorsConstants.Origin];
if (!OriginIsAllowed(origin, policy))
{
return;
}
var accessControlRequestMethod = context.Request.Headers[Microsoft.AspNet.Cors.Infrastructure.CorsConstants.AccessControlRequestMethod];
if (StringValues.IsNullOrEmpty(accessControlRequestMethod))
{
return;
}
var requestHeaders =
context.Request.Headers.GetCommaSeparatedValues(Microsoft.AspNet.Cors.Infrastructure.CorsConstants.AccessControlRequestHeaders);
if (!policy.AllowAnyMethod && !policy.Methods.Contains(accessControlRequestMethod))
{
return;
}
if (!policy.AllowAnyHeader &&
requestHeaders != null &&
!requestHeaders.All(header => Microsoft.AspNet.Cors.Infrastructure.CorsConstants.SimpleRequestHeaders.Contains(header, StringComparer.OrdinalIgnoreCase) ||
policy.Headers.Contains(header, StringComparer.OrdinalIgnoreCase)))
{
return;
}
AddOriginToResult(origin, policy, result);
result.SupportsCredentials = policy.SupportsCredentials;
result.PreflightMaxAge = policy.PreflightMaxAge;
result.AllowedMethods.Add(accessControlRequestMethod);
AddHeaderValues(result.AllowedHeaders, requestHeaders);
}
/// <inheritdoc />
public virtual void ApplyResult(CorsResult result, HttpResponse response)
{
if (result == null)
{
throw new ArgumentNullException(nameof(result));
}
if (response == null)
{
throw new ArgumentNullException(nameof(response));
}
var headers = response.Headers;
if (result.AllowedOrigin != null)
{
headers[Microsoft.AspNet.Cors.Infrastructure.CorsConstants.AccessControlAllowOrigin] = result.AllowedOrigin;
}
if (result.VaryByOrigin)
{
headers["Vary"] = "Origin";
}
if (result.SupportsCredentials)
{
headers[Microsoft.AspNet.Cors.Infrastructure.CorsConstants.AccessControlAllowCredentials] = "true";
}
if (result.AllowedMethods.Count > 0)
{
// Filter out simple methods
var nonSimpleAllowMethods = result.AllowedMethods
.Where(m =>
!Microsoft.AspNet.Cors.Infrastructure.CorsConstants.SimpleMethods.Contains(m, StringComparer.OrdinalIgnoreCase))
.ToArray();
if (nonSimpleAllowMethods.Length > 0)
{
headers.SetCommaSeparatedValues(
Microsoft.AspNet.Cors.Infrastructure.CorsConstants.AccessControlAllowMethods,
nonSimpleAllowMethods);
}
}
if (result.AllowedHeaders.Count > 0)
{
// Filter out simple request headers
var nonSimpleAllowRequestHeaders = result.AllowedHeaders
.Where(header =>
!Microsoft.AspNet.Cors.Infrastructure.CorsConstants.SimpleRequestHeaders.Contains(header, StringComparer.OrdinalIgnoreCase))
.ToArray();
if (nonSimpleAllowRequestHeaders.Length > 0)
{
headers.SetCommaSeparatedValues(
Microsoft.AspNet.Cors.Infrastructure.CorsConstants.AccessControlAllowHeaders,
nonSimpleAllowRequestHeaders);
}
}
if (result.AllowedExposedHeaders.Count > 0)
{
// Filter out simple response headers
var nonSimpleAllowResponseHeaders = result.AllowedExposedHeaders
.Where(header =>
!Microsoft.AspNet.Cors.Infrastructure.CorsConstants.SimpleResponseHeaders.Contains(header, StringComparer.OrdinalIgnoreCase))
.ToArray();
if (nonSimpleAllowResponseHeaders.Length > 0)
{
headers.SetCommaSeparatedValues(
Microsoft.AspNet.Cors.Infrastructure.CorsConstants.AccessControlExposeHeaders,
nonSimpleAllowResponseHeaders);
}
}
if (result.PreflightMaxAge.HasValue)
{
headers[Microsoft.AspNet.Cors.Infrastructure.CorsConstants.AccessControlMaxAge]
= result.PreflightMaxAge.Value.TotalSeconds.ToString(CultureInfo.InvariantCulture);
}
}
protected virtual bool OriginIsAllowed(string origin, CorsPolicy policy)
{
if (!string.IsNullOrWhiteSpace(origin) &&
(policy.AllowAnyOrigin ||
policy.Origins.Contains(origin) ||
IsWildCardSubdomainMatch(origin, policy)))
return true;
return false;
}
private void AddOriginToResult(string origin, CorsPolicy policy, CorsResult result)
{
if (policy.AllowAnyOrigin)
{
if (policy.SupportsCredentials)
{
result.AllowedOrigin = origin;
result.VaryByOrigin = true;
}
else
{
result.AllowedOrigin = Microsoft.AspNet.Cors.Infrastructure.CorsConstants.AnyOrigin;
}
}
else
{
result.AllowedOrigin = origin;
}
}
private static void AddHeaderValues(IList<string> target, IEnumerable<string> headerValues)
{
if (headerValues == null)
{
return;
}
foreach (var current in headerValues)
{
target.Add(current);
}
}
private bool IsWildCardSubdomainMatch(string origin, CorsPolicy policy)
{
var actualOriginUri = new Uri(origin);
var actualOriginRootDomain = GetRootDomain(actualOriginUri);
foreach (var o in policy.Origins)
{
if (!o.Contains("*"))
continue;
// 1) CANNOT USE System.Text.RegularExpression since it does not exist in .net platform 5.4 (which the Microsoft.AspNet.Cors project.json targets)
// 2) '*' char is not valid for creation of a URI object so we replace it just for this comparison
var allowedOriginUri = new Uri(o.Replace("*", "SOMELETTERS"));
if (allowedOriginUri.Scheme == actualOriginUri.Scheme &&
actualOriginRootDomain == GetRootDomain(allowedOriginUri))
return true;
}
return false;
}
private string GetRootDomain(Uri uri)
{
//Got this snippet here http://stackoverflow.com/questions/16473838/get-domain-name-of-a-url-in-c-sharp-net
var host = uri.Host;
int index = host.LastIndexOf('.'), last = 3;
while (index > 0 && index >= last - 3)
{
last = index;
index = host.LastIndexOf('.', last - 1);
}
return host.Substring(index + 1);
}
}
/// <summary>
/// Needed to copy these in since some of them are internal to the Microsoft.AspNet.Cors project
/// </summary>
public static class CorsConstants
{
/// <summary>The HTTP method for the CORS preflight request.</summary>
public static readonly string PreflightHttpMethod = "OPTIONS";
/// <summary>The Origin request header.</summary>
public static readonly string Origin = "Origin";
/// <summary>
/// The value for the Access-Control-Allow-Origin response header to allow all origins.
/// </summary>
public static readonly string AnyOrigin = "*";
/// <summary>The Access-Control-Request-Method request header.</summary>
public static readonly string AccessControlRequestMethod = "Access-Control-Request-Method";
/// <summary>The Access-Control-Request-Headers request header.</summary>
public static readonly string AccessControlRequestHeaders = "Access-Control-Request-Headers";
/// <summary>The Access-Control-Allow-Origin response header.</summary>
public static readonly string AccessControlAllowOrigin = "Access-Control-Allow-Origin";
/// <summary>The Access-Control-Allow-Headers response header.</summary>
public static readonly string AccessControlAllowHeaders = "Access-Control-Allow-Headers";
/// <summary>The Access-Control-Expose-Headers response header.</summary>
public static readonly string AccessControlExposeHeaders = "Access-Control-Expose-Headers";
/// <summary>The Access-Control-Allow-Methods response header.</summary>
public static readonly string AccessControlAllowMethods = "Access-Control-Allow-Methods";
/// <summary>The Access-Control-Allow-Credentials response header.</summary>
public static readonly string AccessControlAllowCredentials = "Access-Control-Allow-Credentials";
/// <summary>The Access-Control-Max-Age response header.</summary>
public static readonly string AccessControlMaxAge = "Access-Control-Max-Age";
internal static readonly string[] SimpleRequestHeaders = new string[4]
{
"Origin",
"Accept",
"Accept-Language",
"Content-Language"
};
internal static readonly string[] SimpleResponseHeaders = new string[6]
{
"Cache-Control",
"Content-Language",
"Content-Type",
"Expires",
"Last-Modified",
"Pragma"
};
internal static readonly string[] SimpleMethods = new string[3]
{
"GET",
"HEAD",
"POST"
};
}
}
Enjoy!
The out-of-the-box CorsService uses policy.Origins.Contains(origin) to evaluate a request. So, it does not look like there is a trivial way to do what you require, because the List must contain the origin. You could implement your own ICorsService, inherit what the out-of-the-box CorsService already provides, and tweak the methods to handle the *.mydomain.com wildcard.
Edit Here is what I accomplished using yo aspnet to generate a 1.0.0-rc1-update2 Web Api project. It works. Register your service in Startup.cs (see CorsServiceCollectionExtensions for details.)
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddOptions();
services.TryAdd(
ServiceDescriptor.Transient<ICorsService, MyCorsService>());
services.TryAdd(
ServiceDescriptor.Transient<ICorsPolicyProvider, DefaultCorsPolicyProvider>());
}
public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(minLevel: LogLevel.Verbose);
app.UseCors(corsPolictyBuilder =>
{
corsPolictyBuilder.WithOrigins("*.mydomain.com");
});
app.Run(async context =>
{
await context.Response.WriteAsync(
$"Is Cors? {context.Request.Headers.ContainsKey(CorsConstants.Origin)}");
});
}
}
Here is the service, awaiting your implementation. You can either copy/paste or inherit from CorsService.
public class MyCorsService : CorsService, ICorsService
{
private ILogger _logger;
public MyCorsService(IOptions<CorsOptions> options, ILogger<MyCorsService> logger)
: base(options)
{
_logger = logger;
_logger.LogInformation("MyCorsService");
}
public override void ApplyResult(
CorsResult result, HttpResponse response)
{
_logger.LogInformation("ApplyResult");
base.ApplyResult(result, response);
}
public override void EvaluateRequest(
HttpContext context, CorsPolicy policy, CorsResult result)
{
_logger.LogInformation("EvaluateRequest");
base.EvaluateRequest(context, policy, result);
}
public override void EvaluatePreflightRequest(
HttpContext context, CorsPolicy policy, CorsResult result)
{
_logger.LogInformation("EvaluatePreflightRequest");
base.EvaluatePreflightRequest(context, policy, result);
}
}
SetIsOriginAllowedToAllowWildcardSubdomains function works well when the wildcard character is specified at the first part of the subdomains for e.g. https:\\*.modules.features.releaseversion.companyname.com but the same function doesn't give the desired result when the wildcard character is specified at any other part of the subdomain for e.g. https:\\environment.modules.*.releaseversion.companyname.com or https:\\*.modules.*.releaseversion.companyname.com or https:\\environment.*.*.releaseversion.companyname.com
Below code is inspired from the #Shaun Luttin and #sjdirect code snippet
We just wanted to extend the behaviour of the Microsoft.AspNetCore.Cors.Infrastructure.CorsService class to enable usage of wildcard character specified anywhere in the URL
Below class performs the CORS check to allow wild card subdomains. Copy this class locally into the desired project. It's an extended version of the Microsoft.AspNetCore.Cors.Infrastructure.CorsService to enable wild card support for subdomains.
using Microsoft.AspNetCore.Cors.Infrastructure;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using System;
using System.Text.RegularExpressions;
namespace Microsoft.AspNetCore.Cors.Infrastructure
{
public class CORSServiceWildCardSupport : CorsService, ICorsService
{
private readonly CorsOptions _options;
private readonly ILogger _logger;
public CORSServiceWildCardSupport(IOptions<CorsOptions> options, ILoggerFactory loggerFactory) : base(options, loggerFactory)
{
_options = options.Value;
_logger = loggerFactory.CreateLogger<CorsService>();
}
public new virtual CorsResult EvaluatePolicy(HttpContext context, CorsPolicy policy)
{
if (context == null)
{
throw new ArgumentNullException("context");
}
if (policy == null)
{
throw new ArgumentNullException("policy");
}
if (policy.AllowAnyOrigin && policy.SupportsCredentials)
{
throw new ArgumentException(Resource.InsecureConfiguration, "policy");
}
IHeaderDictionary headers = context.Request.Headers;
StringValues origin = headers[CorsConstants.Origin];
bool num = string.Equals(context.Request.Method, CorsConstants.PreflightHttpMethod, StringComparison.OrdinalIgnoreCase);
bool flag = num && headers.ContainsKey(CorsConstants.AccessControlRequestMethod);
CorsResult result = new CorsResult
{
IsPreflightRequest = flag,
IsOriginAllowed = IsWildCardSubdomainMatch(origin, policy)
};
if (flag)
{
EvaluatePreflightRequest(context, policy, result);
}
else
{
EvaluateRequest(context, policy, result);
}
return result;
}
protected virtual IsWildCardSubdomainMatch(string origin, CorsPolicy policy)
{
var actualOrigin = new Uri(origin);
foreach (var o in policy.Origins)
{
if (IsWildcardMatch(actualOrigin, o))
{
return true;
}
}
return false;
}
private bool IsWildcardMatch(Uri actualOrigin, string wildcardUri)
{
if (!wildcardUri.StartsWith(actualOrigin.Scheme))
{
return false;
}
var wildcardUriMinusScheme = wildcardUri.Replace(actualOrigin.Scheme + "://", "");
var regexFirstStage = wildcardUriMinusScheme.Replace(".", "\\.");
var regexAllowedHosts = "^" + regexFirstStage.Replace("*", ".*") + "$";
var actualOriginMinusScheme = actualOrigin.OriginalString.Replace(actualOrigin.Scheme + "://", "");
var isMatch = Regex.IsMatch(actualOriginMinusScheme, regexAllowedHosts);
return isMatch;
}
}
}
From the above class function IsWildCardSubdomainMatch or IsWildcardMatch can be extended based on the requirement, for our requirement we just needed to perform string comparison.
Register the CORSServiceWildCardSupport class into the dependency container using the below extension class. Copy the class locally into the desired project
using Microsoft.AspNetCore.Cors.Infrastructure;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using System;
using System.Collections.Generic;
namespace Microsoft.Extensions.DependencyInjection
{
public static class CORSServiceCollectionExtensions
{
public static IServiceCollection AddCORSWithWildCardSupport(this IServiceCollection services)
{
if (services == null)
{
throw new ArgumentNullException("services");
}
services.AddOptions();
services.TryAdd(ServiceDescriptor.Transient<ICorsService, CORSServiceWildCardSupport>());
services.TryAdd(ServiceDescriptor.Transient<ICorsPolicyProvider, DefaultCorsPolicyProvider>());
return services;
}
public static IServiceCollection AddCORSWithWildCardSupport(this IServiceCollection services, Action<CorsOptions> setupAction)
{
if (services == null)
{
throw new ArgumentNullException("services");
}
if (setupAction == null)
{
throw new ArgumentNullException("setupAction");
}
services.AddCORSWithWildCardSupport();
services.Configure(setupAction);
return services;
}
}
}
Register the CORS from the Startup class
public void ConfigureServices(IServiceCollection services)
{
try
{
string[] whitelist = {"https:\\environment.modules.*.releaseversion.companyname.com","https:\\*.modules.*.releaseversion.companyname.com","https:\\environment.*.*.releaseversion.companyname.com"};
services.AddCORSWithWildCardSupport(o => o.AddPolicy(Resource.CorsPolicyName, builder =>
{
builder.WithOrigins(whitelist)
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials();
}));
services.AddControllers();
services.AddMvc(option => option.EnableEndpointRouting = false)
.SetCompatibilityVersion(CompatibilityVersion.Version_3_0);
services.AddAuthentication("Windows");
}
catch(Exception ex)
{
Logger.Error($"Failed to start due to {ex.Message}.");
}
}
services.AddControllers method also registers ICORSService into the dependency container hence always use AddCORS prior to AddControllers
Enjoy :)

Custom model binder with inheritance using Web API and RavenDB

I'm developing a simple web app where I need to bind all types implementing and interface of a specific type. My interface has one single property like this
public interface IContent {
string Id { get;set; }
}
a common class using this interface would look like this
public class Article : IContent {
public string Id { get;set; }
public string Heading { get;set; }
}
to be clean here the article class is just one of many different classes implementing IContent so therefor I need a generic way of storing and updating these types.
So in my controller I have the put method like this
public void Put(string id, [System.Web.Http.ModelBinding.ModelBinder(typeof(ContentModelBinder))] IContent value)
{
// Store the updated object in ravendb
}
and the ContentBinder
public class ContentModelBinder : System.Web.Http.ModelBinding.IModelBinder {
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext) {
actionContext.ControllerContext.Request.Content.ReadAsAsync<Article>().ContinueWith(task =>
{
Article model = task.Result;
bindingContext.Model = model;
});
return true;
}
}
The code above does not work because it does not seem to get hold of the Heading property even though if I use the default model binder it binds the Heading correctly.
So, in the BindModel method I guess I need to load the correct object from ravendb based on the Id and then update the complex object using some kind of default model binder or so? This is where I need some help.
Marcus, following is an example which would work fine for both Json and Xml formatter.
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Net.Http.Formatting;
using System.Net.Http.Headers;
using System.Runtime.Serialization;
using System.Web.Http;
using System.Web.Http.SelfHost;
namespace Service
{
class Service
{
private static HttpSelfHostServer server = null;
private static string baseAddress = string.Format("http://{0}:9095/", Environment.MachineName);
static void Main(string[] args)
{
HttpSelfHostConfiguration config = new HttpSelfHostConfiguration(baseAddress);
config.Routes.MapHttpRoute("Default", "api/{controller}/{id}", new { id = RouteParameter.Optional });
config.IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.Always;
config.Formatters.JsonFormatter.SerializerSettings.TypeNameHandling = TypeNameHandling.Objects;
try
{
server = new HttpSelfHostServer(config);
server.OpenAsync().Wait();
Console.WriteLine("Service listenting at: {0} ...", baseAddress);
TestWithHttpClient("application/xml");
TestWithHttpClient("application/json");
Console.ReadLine();
}
catch (Exception ex)
{
Console.WriteLine("Exception Details:\n{0}", ex.ToString());
}
finally
{
if (server != null)
{
server.CloseAsync().Wait();
}
}
}
private static void TestWithHttpClient(string mediaType)
{
HttpClient client = new HttpClient();
MediaTypeFormatter formatter = null;
// NOTE: following any settings on the following formatters should match
// to the settings that the service's formatters have.
if (mediaType == "application/xml")
{
formatter = new XmlMediaTypeFormatter();
}
else if (mediaType == "application/json")
{
JsonMediaTypeFormatter jsonFormatter = new JsonMediaTypeFormatter();
jsonFormatter.SerializerSettings.TypeNameHandling = TypeNameHandling.Objects;
formatter = jsonFormatter;
}
HttpRequestMessage request = new HttpRequestMessage();
request.RequestUri = new Uri(baseAddress + "api/students");
request.Method = HttpMethod.Get;
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(mediaType));
HttpResponseMessage response = client.SendAsync(request).Result;
Student std = response.Content.ReadAsAsync<Student>().Result;
Console.WriteLine("GET data in '{0}' format", mediaType);
if (StudentsController.CONSTANT_STUDENT.Equals(std))
{
Console.WriteLine("both are equal");
}
client = new HttpClient();
request = new HttpRequestMessage();
request.RequestUri = new Uri(baseAddress + "api/students");
request.Method = HttpMethod.Post;
request.Content = new ObjectContent<Person>(StudentsController.CONSTANT_STUDENT, formatter);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(mediaType));
Student std1 = client.SendAsync(request).Result.Content.ReadAsAsync<Student>().Result;
Console.WriteLine("POST and receive data in '{0}' format", mediaType);
if (StudentsController.CONSTANT_STUDENT.Equals(std1))
{
Console.WriteLine("both are equal");
}
}
}
public class StudentsController : ApiController
{
public static readonly Student CONSTANT_STUDENT = new Student() { Id = 1, Name = "John", EnrolledCourses = new List<string>() { "maths", "physics" } };
public Person Get()
{
return CONSTANT_STUDENT;
}
// NOTE: specifying FromBody here is not required. By default complextypes are bound
// by formatters which read the body
public Person Post([FromBody] Person person)
{
if (!ModelState.IsValid)
{
throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest, this.ModelState));
}
return person;
}
}
[DataContract]
[KnownType(typeof(Student))]
public abstract class Person : IEquatable<Person>
{
[DataMember]
public int Id { get; set; }
[DataMember]
public string Name { get; set; }
// this is ignored
public DateTime DateOfBirth { get; set; }
public bool Equals(Person other)
{
if (other == null)
return false;
if (ReferenceEquals(this, other))
return true;
if (this.Id != other.Id)
return false;
if (this.Name != other.Name)
return false;
return true;
}
}
[DataContract]
public class Student : Person, IEquatable<Student>
{
[DataMember]
public List<string> EnrolledCourses { get; set; }
public bool Equals(Student other)
{
if (!base.Equals(other))
{
return false;
}
if (this.EnrolledCourses == null && other.EnrolledCourses == null)
{
return true;
}
if ((this.EnrolledCourses == null && other.EnrolledCourses != null) ||
(this.EnrolledCourses != null && other.EnrolledCourses == null))
return false;
if (this.EnrolledCourses.Count != other.EnrolledCourses.Count)
return false;
for (int i = 0; i < this.EnrolledCourses.Count; i++)
{
if (this.EnrolledCourses[i] != other.EnrolledCourses[i])
return false;
}
return true;
}
}
}
I used #kiran-challa solution and added TypeNameHandling on Json media type formatter's SerializerSettings.

Can Client of a WCF Duplex Service(TCP binding) send and recive at the same time?

My code at the moment looks like this:
Server side:
#region IClientCallback interface
interface IClientCallback
{
[OperationContract(IsOneWay = true)]
void ReceiveWcfElement(WcfElement wcfElement);
}
#endregion
#region IService interface
[ServiceContract(SessionMode = SessionMode.Required, CallbackContract = typeof(IClientCallback))]
interface IService
{
[OperationContract(IsOneWay = true, IsInitiating = false, IsTerminating = false)]
void ReadyToReceive(string userName, int source, string ostatniTypWiadomosci);
[OperationContract(IsOneWay = false, IsInitiating = false, IsTerminating = false)]
bool SendWcfElement(WcfElement wcfElement);
[OperationContract(IsOneWay = false, IsInitiating = true, IsTerminating = false)]
List<int> Login(Client name, string password, bool isAuto, bool isSuperMode);
}
#endregion
#region Public enums/event args
public delegate void WcfElementsReceivedFromClientEventHandler(object sender, WcfElementsReceivedFromClientEventArgs e);
public class WcfElementsReceivedFromClientEventArgs : EventArgs
{
public string UserName;
}
public class ServiceEventArgs : EventArgs
{
public WcfElement WcfElement;
public Client Person;
}
#endregion
#region Service
[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single, ConcurrencyMode = ConcurrencyMode.Multiple)]
public class Service : IService
{
#region Instance fields
//thread sync lock object
private static readonly Object SyncObj = new Object();
//callback interface for clients
IClientCallback _callback;
//delegate used for BroadcastEvent
public delegate void ChatEventHandler(object sender, ServiceEventArgs e);
public static event ChatEventHandler ChatEvent;
private ChatEventHandler _myEventHandler;
//holds a list of clients, and a delegate to allow the BroadcastEvent to work
//out which chatter delegate to invoke
static readonly Dictionary<Client, ChatEventHandler> Clients = new Dictionary<Client, ChatEventHandler>();
//current person
private Client _client;
#endregion
#region Helpers
private bool CheckIfPersonExists(string name)
{
return Clients.Keys.Any(p => p.UserName.Equals(name, StringComparison.OrdinalIgnoreCase));
}
private ChatEventHandler getPersonHandler(string name)
{
foreach (var c in Clients.Keys.Where(c => c.UserName.Equals(name, StringComparison.OrdinalIgnoreCase)))
{
ChatEventHandler chatTo;
Clients.TryGetValue(c, out chatTo);
return chatTo;
}
return null;
}
private Client GetPerson(string name)
{
return Clients.Keys.FirstOrDefault(c => c.UserName.Equals(name, StringComparison.OrdinalIgnoreCase));
}
#endregion
#region IService implementation
public List<int> Login(Client client, string password, bool isAuto, bool isSuperMode)
{
if (client.ElementsVersions == null)
{
client.ElementsVersions = new WcfElement(WcfElement.RodzajWiadomosci.VersionControl, client.UserName);
}
//create a new ChatEventHandler delegate, pointing to the MyEventHandler() method
_myEventHandler = MyEventHandler;
lock (SyncObj)
{
if (!CheckIfPersonExists(client.UserName))
{
_client = client;
Clients.Add(client, _myEventHandler);
}
else
{
_client = client;
foreach (var c in Clients.Keys.Where(c => c.UserName.Equals(client.UserName)))
{
ChatEvent -= Clients[c];
Clients.Remove(c);
break;
}
Clients[client] = _myEventHandler;
}
_client.LockObj = new object();
}
_callback = OperationContext.Current.GetCallbackChannel<IClientCallback>();
ChatEvent += _myEventHandler;
var rValue = isAuto ? bazaDanych.Login(client.UserName, isSuperMode) : bazaDanych.Login(client.UserName, password);
return rValue;
}
public void PerformDataSync(Client c)
{
WcfElement wcfDelete = null;
WcfElement wcfUpdate = null;
//...
//this method prepares elements for client
//when done it adds them to clients queue (List<WcfElement)
try
{
var counter = 0;
if (wcfDelete != null)
{
foreach (var wcf in WcfElement.SplitWcfElement(wcfDelete, false))//split message into small ones
{
c.AddElementToQueue(wcf, counter++);
}
}
if (wcfUpdate != null)
{
foreach (var wcf in WcfElement.SplitWcfElement(wcfUpdate, true))
{
c.AddElementToQueue(wcf, counter++);
}
}
SendMessageToGui(string.Format("Wstępna synchronizacja użytkownika {0} zakończona.", c.UserName));
c.IsSynchronized = true;
}
catch (Exception e)
{
}
}
private void SendMessageToClient(object sender, EventArgs e)
{
var c = (Client) sender;
if (c.IsReceiving || c.IsSending)
{
return;
}
c.IsReceiving = true;
var wcfElement = c.GetFirstElementFromQueue();
if (wcfElement == null)
{
c.IsReceiving = false;
return;
}
Clients[c].Invoke(this, new ServiceEventArgs { Person = c, WcfElement = wcfElement });
}
public void ReadyToReceive(string userName)
{
var c = GetPerson(userName);
c.IsSending = false;
c.IsReceiving = false;
if (c.IsSynchronized)
{
SendMessageToClient(c, null);
}
else
{
PerformDataSync(c);
}
}
public bool SendWcfElement(WcfElement wcfElement)
{
var cl = GetPerson(wcfElement.UserName);
cl.IsSending = true;
if (wcfElement.WcfElementVersion != bazaDanych.WcfElementVersion) return false;
//method processes messages and if needed creates creates WcfElements which are added to every clients queue
return ifSuccess;
}
#endregion
#region private methods
private void MyEventHandler(object sender, ServiceEventArgs e)
{
try
{
_callback.ReceiveWcfElement(e.WcfElement);
}
catch (Exception ex)
{
}
}
#endregion
}
#endregion
Client side in a moment
#region Client class
[DataContract]
public class Client
{
#region Instance Fields
/// <summary>
/// The UserName
/// </summary>
[DataMember]
public string UserName { get; set; }
[DataMember]
public WcfElement ElementsVersions { get; set; }
private bool _isSynchronized;
public bool IsSynchronized
{
get { return _isSynchronized; }
set
{
_isSynchronized = value;
}
}
public bool IsSending { get; set; }
public bool IsReceiving { get; set; }
private List<WcfElement> ElementsQueue { get; set; }
public object LockObj { get; set; }
public void AddElementToQueue(WcfElement wcfElement, int position = -1)
{
try
{
lock (LockObj)
{
if (ElementsQueue == null) ElementsQueue = new List<WcfElement>();
if (position != -1 && position <= ElementsQueue.Count)
{
try
{
ElementsQueue.Insert(position, wcfElement);
}
catch (Exception e)
{
}
}
else
{
try
{
//dodaje na koncu
ElementsQueue.Add(wcfElement);
}
catch (Exception e)
{
}
}
}
}
catch (Exception e)
{
}
}
public WcfElement GetFirstElementFromQueue()
{
if (ElementsQueue == null) return null;
if (ElementsQueue.Count > 0)
{
var tmp = ElementsQueue[0];
ElementsQueue.RemoveAt(0);
return tmp;
}
return null;
}
#endregion
#region Ctors
/// <summary>
/// Assign constructor
/// </summary>
/// <param name="userName">The userName to use for this client</param>
public Client(string userName)
{
UserName = userName;
}
#endregion
}
#endregion
ProxySingletion:
[CallbackBehavior(ConcurrencyMode = ConcurrencyMode.Reentrant, UseSynchronizationContext = false)]
public sealed class ProxySingleton : IClientCallback
{
#region Instance Fields
private static ProxySingleton _singleton;
public static bool IsConnected;
private static readonly object SingletonLock = new object();
private ServiceProxy _proxy;
private Client _myPerson;
private delegate void HandleDelegate(Client[] list);
private delegate void HandleErrorDelegate();
//main proxy event
public delegate void ProxyEventHandler(object sender, ProxyEventArgs e);
public static event ProxyEventHandler ProxyEvent;
//callback proxy event
public delegate void ProxyCallBackEventHandler(object sender, ProxyCallBackEventArgs e);
public static event ProxyCallBackEventHandler ProxyCallBackEvent;
#endregion
#region Ctor
/// <summary>
/// Blank constructor
/// </summary>
private ProxySingleton()
{
}
#endregion
#region Public Methods
#region IClientCallback implementation
public void ReceiveWcfElement(WcfElement wcfElement)
{
//process received data
//...
ReadyToReceive();
}
#endregion
public void ReadyToReceive()
{
try
{
if (bazaDanych.Dane.Client.IsSending) return;
var w = bazaDanych.Dane.Client.GetFirstElementFromQueue();
if (w != null)
{
SendWcfElement(w);
return;
}
_proxy.ReadyToReceive(bazaDanych.Dane.Client.UserName, source, ostatniTypWiadomosci);
}
catch (Exception)
{
IsConnected = false;
}
}
public static WcfElement CurrentWcfElement;
public bool SendWcfElement(WcfElement wcfElement)
{
if (bazaDanych.Dane.Client.IsReceiving)
{
bazaDanych.Dane.Client.AddElementToQueue(wcfElement);
return true;
}
bazaDanych.Dane.Client.IsSending = true;
foreach (var wcfElementSplited in WcfElement.SplitWcfElement(wcfElement, true))
{
CurrentWcfElement = wcfElementSplited;
try
{
var r = _proxy.SendWcfElement(wcfElementSplited);
CurrentWcfElement = null;
}
catch (Exception e)
{
IsConnected = false;
return false;
}
}
bazaDanych.Dane.Client.IsSending = false;
ReadyToReceive();
return true;
}
public void ListenForConnectOrReconnect(EventArgs e)
{
SendWcfElement(WcfElement.GetVersionElement());//send wcfelement for perform PerformDataSync
ReadyToReceive();
}
public static bool IsReconnecting;
public bool ConnectOrReconnect(bool shouldRaiseEvent = true)
{
if (IsReconnecting)
{
return IsConnected;
}
if (IsConnected) return true;
IsReconnecting = true;
bazaDanych.Dane.Client.IsReceiving = false;
bazaDanych.Dane.Client.IsSending = false;
bazaDanych.Dane.Client.IsSynchronized = false;
try
{
var site = new InstanceContext(this);
_proxy = new ServiceProxy(site);
var list = _proxy.Login(bazaDanych.Dane.Client, bazaDanych.Dane.UserPassword, bazaDanych.Dane.UserIsAuto, bazaDanych.Dane.UserIsSuperMode);
bazaDanych.Dane.UserRights.Clear();
bazaDanych.Dane.UserRights.AddRange(list);
IsConnected = true;
if (shouldRaiseEvent) ConnectOrReconnectEvent(null);
}
catch (Exception e)
{
IsConnected = false;
}
IsReconnecting = false;
return IsConnected;
}
}
#endregion
At the moment my app works like this:
After successful login every client sends WcfElements(which contains bunch of list with ids and versions of elements). Then it sends ReadyToReceive one way message which after login fires performsync method. That method prepares data for client and sends first of them using one way receive method. IF there is more than one wcfelement to send then only last one is marked as last. Client responds with ReadyToReceive after every successful receive from Server. All up to this point works quite well. Problem starts later. Mostly packages are lost (method receiveWcfElement). Server has marked that client is receiving and maybe processing message and is waitng for readytoreceive packet, which will never be send because of lost element.
I've made it like this because as far as I know client can't send and receive at the same time. I've tried this and got this problem:
If client send wcfElement with SendWcfElement method and server due to processing this element created another element which was supposed to be ssend back to client then client whoud have faulted proxy if callback was send before sendWcfElement returned true indicating that method was completed.
Now I wonder if it is possible for client to send and receive at the same time using two way methods ?
I ended up with to services(two connections). One for connection from client to server and another with callback which handles connection from server to client.