#272 Make more properties settable (Items, RequestAborted, IsHttps, Query, Cookies).

This commit is contained in:
Chris R 2015-05-12 11:02:58 -07:00
parent c4df4a0a15
commit dce1d0e88f
18 changed files with 308 additions and 125 deletions

View File

@ -17,22 +17,22 @@ namespace Microsoft.AspNet.Http
public abstract ConnectionInfo Connection { get; }
public abstract WebSocketManager WebSockets { get; }
public abstract AuthenticationManager Authentication { get; }
public abstract ClaimsPrincipal User { get; set; }
public abstract IDictionary<object, object> Items { get; }
public abstract IDictionary<object, object> Items { get; set; }
public abstract IServiceProvider ApplicationServices { get; set; }
public abstract IServiceProvider RequestServices { get; set; }
public abstract CancellationToken RequestAborted { get; }
public abstract CancellationToken RequestAborted { get; set; }
public abstract ISessionCollection Session { get; }
public abstract WebSocketManager WebSockets { get; }
public abstract void Abort();
public abstract void Dispose();

View File

@ -27,7 +27,7 @@ namespace Microsoft.AspNet.Http
/// Returns true if the owin.RequestScheme is https.
/// </summary>
/// <returns>true if this request is using https; otherwise, false.</returns>
public abstract bool IsHttps { get; }
public abstract bool IsHttps { get; set; }
/// <summary>
/// Gets or set the Host header. May include the port.
@ -57,7 +57,7 @@ namespace Microsoft.AspNet.Http
/// Gets the query value collection parsed from owin.RequestQueryString.
/// </summary>
/// <returns>The query value collection parsed from owin.RequestQueryString.</returns>
public abstract IReadableStringCollection Query { get; }
public abstract IReadableStringCollection Query { get; set; }
/// <summary>
/// Gets or set the owin.RequestProtocol.
@ -75,7 +75,7 @@ namespace Microsoft.AspNet.Http
/// Gets the collection of Cookies for this request.
/// </summary>
/// <returns>The collection of Cookies for this request.</returns>
public abstract IReadableStringCollection Cookies { get; }
public abstract IReadableStringCollection Cookies { get; set; }
/// <summary>
/// Gets or sets the Content-Length header

View File

@ -111,6 +111,7 @@ namespace Microsoft.AspNet.Http
/// </summary>
/// <param name="name">The un-encoded parameter name</param>
/// <param name="value">The un-encoded parameter value</param>
/// <returns>The resulting QueryString</returns>
public static QueryString Create(string name, string value)
{
return new QueryString("?" + UrlEncoder.Default.UrlEncode(name) + '=' + UrlEncoder.Default.UrlEncode(value));
@ -120,7 +121,7 @@ namespace Microsoft.AspNet.Http
/// Creates a query string composed from the given name value pairs.
/// </summary>
/// <param name="parameters"></param>
/// <returns></returns>
/// <returns>The resulting QueryString</returns>
public static QueryString Create(IEnumerable<KeyValuePair<string, string>> parameters)
{
var builder = new StringBuilder();
@ -137,6 +138,30 @@ namespace Microsoft.AspNet.Http
return new QueryString(builder.ToString());
}
/// <summary>
/// Creates a query string composed from the given name value pairs.
/// </summary>
/// <param name="parameters"></param>
/// <returns>The resulting QueryString</returns>
public static QueryString Create(IEnumerable<KeyValuePair<string, string[]>> parameters)
{
var builder = new StringBuilder();
bool first = true;
foreach (var pair in parameters)
{
foreach (var value in pair.Value)
{
builder.Append(first ? "?" : "&");
first = false;
builder.Append(UrlEncoder.Default.UrlEncode(pair.Key));
builder.Append("=");
builder.Append(UrlEncoder.Default.UrlEncode(value));
}
}
return new QueryString(builder.ToString());
}
public QueryString Add(QueryString other)
{
if (!HasValue || Value.Equals("?", StringComparison.Ordinal))

View File

@ -7,7 +7,7 @@ namespace Microsoft.AspNet.Http.Features
{
public interface IHttpRequestLifetimeFeature
{
CancellationToken RequestAborted { get; }
CancellationToken RequestAborted { get; set; }
void Abort();
}
}

View File

@ -5,7 +5,8 @@ namespace Microsoft.AspNet.Http.Internal
{
internal static class Constants
{
internal const string Https = "HTTPS";
internal const string Http = "http";
internal const string Https = "https";
internal static class BuilderProperties
{

View File

@ -69,7 +69,7 @@ namespace Microsoft.AspNet.Http.Internal
private IHttpRequestLifetimeFeature LifetimeFeature
{
get { return _lifetime.Fetch(_features); }
get { return _lifetime.Fetch(_features) ?? _lifetime.Update(_features, new HttpRequestLifetimeFeature()); }
}
private ISessionFeature SessionFeature
@ -103,6 +103,7 @@ namespace Microsoft.AspNet.Http.Internal
public override IDictionary<object, object> Items
{
get { return ItemsFeature.Items; }
set { ItemsFeature.Items = value; }
}
public override IServiceProvider ApplicationServices
@ -117,19 +118,10 @@ namespace Microsoft.AspNet.Http.Internal
set { ServiceProvidersFeature.RequestServices = value; }
}
public int Revision { get { return _features.Revision; } }
public override CancellationToken RequestAborted
{
get
{
var lifetime = LifetimeFeature;
if (lifetime != null)
{
return lifetime.RequestAborted;
}
return CancellationToken.None;
}
get { return LifetimeFeature.RequestAborted; }
set { LifetimeFeature.RequestAborted = value; }
}
public override ISessionCollection Session
@ -167,11 +159,7 @@ namespace Microsoft.AspNet.Http.Internal
public override void Abort()
{
var lifetime = LifetimeFeature;
if (lifetime != null)
{
lifetime.Abort();
}
LifetimeFeature.Abort();
}
public override void Dispose()

View File

@ -101,6 +101,7 @@ namespace Microsoft.AspNet.Http.Internal
public override bool IsHttps
{
get { return string.Equals(Constants.Https, Scheme, StringComparison.OrdinalIgnoreCase); }
set { Scheme = value ? Constants.Https : Constants.Http; }
}
public override HostString Host
@ -112,6 +113,7 @@ namespace Microsoft.AspNet.Http.Internal
public override IReadableStringCollection Query
{
get { return QueryFeature.Query; }
set { QueryFeature.Query = value; }
}
public override string Protocol
@ -128,6 +130,7 @@ namespace Microsoft.AspNet.Http.Internal
public override IReadableStringCollection Cookies
{
get { return RequestCookiesFeature.Cookies; }
set { RequestCookiesFeature.Cookies = value; }
}
public override string ContentType

View File

@ -0,0 +1,17 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Threading;
using Microsoft.AspNet.Http.Features;
namespace Microsoft.AspNet.Http.Features.Internal
{
public class HttpRequestLifetimeFeature : IHttpRequestLifetimeFeature
{
public CancellationToken RequestAborted { get; set; }
public void Abort()
{
}
}
}

View File

@ -7,6 +7,6 @@ namespace Microsoft.AspNet.Http.Features.Internal
{
public interface IItemsFeature
{
IDictionary<object, object> Items { get; }
IDictionary<object, object> Items { get; set; }
}
}

View File

@ -5,6 +5,6 @@ namespace Microsoft.AspNet.Http.Features.Internal
{
public interface IQueryFeature
{
IReadableStringCollection Query { get; }
IReadableStringCollection Query { get; set; }
}
}

View File

@ -5,6 +5,6 @@ namespace Microsoft.AspNet.Http.Features.Internal
{
public interface IRequestCookiesFeature
{
IReadableStringCollection Cookies { get; }
IReadableStringCollection Cookies { get; set; }
}
}

View File

@ -13,6 +13,6 @@ namespace Microsoft.AspNet.Http.Features.Internal
Items = new ItemsDictionary();
}
public IDictionary<object, object> Items { get; private set; }
public IDictionary<object, object> Items { get; set; }
}
}

View File

@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using Microsoft.AspNet.FeatureModel;
using Microsoft.AspNet.Http.Internal;
@ -41,13 +42,22 @@ namespace Microsoft.AspNet.Http.Features.Internal
}
var queryString = _request.Fetch(_features).QueryString;
if (_query == null || _queryString != queryString)
if (_query == null || !string.Equals(_queryString, queryString, StringComparison.Ordinal))
{
_queryString = queryString;
_query = new ReadableStringCollection(QueryHelpers.ParseQuery(queryString));
}
return _query;
}
set
{
_query = value;
if (_features != null)
{
_queryString = _query == null ? string.Empty : QueryString.Create(_query).ToString();
_request.Fetch(_features).QueryString = _queryString;
}
}
}
}
}

View File

@ -50,19 +50,37 @@ namespace Microsoft.AspNet.Http.Features.Internal
values = new string[0];
}
if (_cookiesCollection == null)
{
_cookieHeaders = values;
_cookiesCollection = new RequestCookiesCollection();
_cookiesCollection.Reparse(values);
}
else if (!Enumerable.SequenceEqual(_cookieHeaders, values, StringComparer.Ordinal))
if (_cookieHeaders == null || !Enumerable.SequenceEqual(_cookieHeaders, values, StringComparer.Ordinal))
{
_cookieHeaders = values;
if (_cookiesCollection == null)
{
_cookiesCollection = new RequestCookiesCollection();
_cookies = _cookiesCollection;
}
_cookiesCollection.Reparse(values);
}
return _cookiesCollection;
return _cookies;
}
set
{
_cookies = value;
_cookieHeaders = null;
_cookiesCollection = _cookies as RequestCookiesCollection;
if (_cookies != null && _features != null)
{
var headers = new List<string>();
foreach (var pair in _cookies)
{
foreach (var cookieValue in pair.Value)
{
headers.Add(new CookieHeaderValue(pair.Key, cookieValue).ToString());
}
}
_cookieHeaders = headers.ToArray();
_request.Fetch(_features).Headers[HeaderNames.Cookie] = _cookieHeaders;
}
}
}
}

View File

@ -247,6 +247,7 @@ namespace Microsoft.AspNet.Owin
CancellationToken IHttpRequestLifetimeFeature.RequestAborted
{
get { return Prop<CancellationToken>(OwinConstants.CallCancelled); }
set { Prop(OwinConstants.CallCancelled, value); }
}
void IHttpRequestLifetimeFeature.Abort()

View File

@ -0,0 +1,111 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNet.Http.Features.Authentication;
using Microsoft.AspNet.Http.Features.Authentication.Internal;
using Microsoft.AspNet.Http.Internal;
using Xunit;
namespace Microsoft.AspNet.Http.Authentication.Internal
{
public class AuthenticationManagerTests
{
[Fact]
public async Task AuthenticateWithNoAuthMiddlewareThrows()
{
var context = CreateContext();
Assert.Throws<InvalidOperationException>(() => context.Authentication.Authenticate("Foo"));
await Assert.ThrowsAsync<InvalidOperationException>(async () => await context.Authentication.AuthenticateAsync("Foo"));
}
[Fact]
public void ChallengeWithNoAuthMiddlewareMayThrow()
{
var context = CreateContext();
context.Authentication.Challenge();
Assert.Equal(401, context.Response.StatusCode);
Assert.Throws<InvalidOperationException>(() => context.Authentication.Challenge("Foo"));
}
[Fact]
public void SignInWithNoAuthMiddlewareThrows()
{
var context = CreateContext();
Assert.Throws<InvalidOperationException>(() => context.Authentication.SignIn("Foo", new ClaimsPrincipal()));
}
[Fact]
public void SignOutWithNoAuthMiddlewareMayThrow()
{
var context = CreateContext();
context.Authentication.SignOut();
Assert.Throws<InvalidOperationException>(() => context.Authentication.SignOut("Foo"));
}
[Fact]
public void SignInOutIn()
{
var context = CreateContext();
var handler = new AuthHandler();
context.SetFeature<IHttpAuthenticationFeature>(new HttpAuthenticationFeature() { Handler = handler });
var user = new ClaimsPrincipal();
context.Authentication.SignIn("ignored", user);
Assert.True(handler.SignedIn);
context.Authentication.SignOut("ignored");
Assert.False(handler.SignedIn);
context.Authentication.SignIn("ignored", user);
Assert.True(handler.SignedIn);
context.Authentication.SignOut("ignored", new AuthenticationProperties() { RedirectUri = "~/logout" });
Assert.False(handler.SignedIn);
}
private class AuthHandler : IAuthenticationHandler
{
public bool SignedIn { get; set; }
public void Authenticate(AuthenticateContext context)
{
throw new NotImplementedException();
}
public Task AuthenticateAsync(AuthenticateContext context)
{
throw new NotImplementedException();
}
public void Challenge(ChallengeContext context)
{
throw new NotImplementedException();
}
public void GetDescriptions(DescribeSchemesContext context)
{
throw new NotImplementedException();
}
public void SignIn(SignInContext context)
{
SignedIn = true;
context.Accept();
}
public void SignOut(SignOutContext context)
{
SignedIn = false;
context.Accept();
}
}
private HttpContext CreateContext()
{
var context = new DefaultHttpContext();
return context;
}
}
}

View File

@ -1,14 +1,11 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNet.FeatureModel;
using Microsoft.AspNet.Http.Authentication;
using Microsoft.AspNet.Http.Features.Authentication;
using Microsoft.AspNet.Http.Features.Authentication.Internal;
using Microsoft.AspNet.Http.Features.Internal;
using Xunit;
namespace Microsoft.AspNet.Http.Internal
@ -44,91 +41,31 @@ namespace Microsoft.AspNet.Http.Internal
}
[Fact]
public async Task AuthenticateWithNoAuthMiddlewareThrows()
public void GetItems_DefaultCollectionProvided()
{
var context = CreateContext();
Assert.Throws<InvalidOperationException>(() => context.Authentication.Authenticate("Foo"));
await Assert.ThrowsAsync<InvalidOperationException>(async () => await context.Authentication.AuthenticateAsync("Foo"));
var context = new DefaultHttpContext(new FeatureCollection());
Assert.Null(context.GetFeature<IItemsFeature>());
var items = context.Items;
Assert.NotNull(context.GetFeature<IItemsFeature>());
Assert.NotNull(items);
Assert.Same(items, context.Items);
var item = new object();
context.Items["foo"] = item;
Assert.Same(item, context.Items["foo"]);
}
[Fact]
public void ChallengeWithNoAuthMiddlewareMayThrow()
public void SetItems_NewCollectionUsed()
{
var context = CreateContext();
context.Authentication.Challenge();
Assert.Equal(401, context.Response.StatusCode);
Assert.Throws<InvalidOperationException>(() => context.Authentication.Challenge("Foo"));
}
[Fact]
public void SignInWithNoAuthMiddlewareThrows()
{
var context = CreateContext();
Assert.Throws<InvalidOperationException>(() => context.Authentication.SignIn("Foo", new ClaimsPrincipal()));
}
[Fact]
public void SignOutWithNoAuthMiddlewareMayThrow()
{
var context = CreateContext();
context.Authentication.SignOut();
Assert.Throws<InvalidOperationException>(() => context.Authentication.SignOut("Foo"));
}
[Fact]
public void SignInOutIn()
{
var context = CreateContext();
var handler = new AuthHandler();
context.SetFeature<IHttpAuthenticationFeature>(new HttpAuthenticationFeature() { Handler = handler });
var user = new ClaimsPrincipal();
context.Authentication.SignIn("ignored", user);
Assert.True(handler.SignedIn);
context.Authentication.SignOut("ignored");
Assert.False(handler.SignedIn);
context.Authentication.SignIn("ignored", user);
Assert.True(handler.SignedIn);
context.Authentication.SignOut("ignored", new AuthenticationProperties() { RedirectUri = "~/logout" });
Assert.False(handler.SignedIn);
}
private class AuthHandler : IAuthenticationHandler
{
public bool SignedIn { get; set; }
public void Authenticate(AuthenticateContext context)
{
throw new NotImplementedException();
}
public Task AuthenticateAsync(AuthenticateContext context)
{
throw new NotImplementedException();
}
public void Challenge(ChallengeContext context)
{
throw new NotImplementedException();
}
public void GetDescriptions(DescribeSchemesContext context)
{
throw new NotImplementedException();
}
public void SignIn(SignInContext context)
{
SignedIn = true;
context.Accept();
}
public void SignOut(SignOutContext context)
{
SignedIn = false;
context.Accept();
}
var context = new DefaultHttpContext(new FeatureCollection());
Assert.Null(context.GetFeature<IItemsFeature>());
var items = new Dictionary<object, object>();
context.Items = items;
Assert.NotNull(context.GetFeature<IItemsFeature>());
Assert.Same(items, context.Items);
var item = new object();
items["foo"] = item;
Assert.Same(item, context.Items["foo"]);
}
private HttpContext CreateContext()

View File

@ -115,6 +115,78 @@ namespace Microsoft.AspNet.Http.Internal
Assert.Equal(expected, headers["Host"][0]);
}
[Fact]
public void IsHttps_CorrectlyReflectsScheme()
{
var request = new DefaultHttpContext().Request;
Assert.Equal(string.Empty, request.Scheme);
Assert.False(request.IsHttps);
request.IsHttps = true;
Assert.Equal("https", request.Scheme);
request.IsHttps = false;
Assert.Equal("http", request.Scheme);
request.Scheme = "ftp";
Assert.False(request.IsHttps);
request.Scheme = "HTTPS";
Assert.True(request.IsHttps);
}
[Fact]
public void Query_GetAndSet()
{
var request = new DefaultHttpContext().Request;
var requestFeature = request.HttpContext.GetFeature<IHttpRequestFeature>();
Assert.Equal(string.Empty, requestFeature.QueryString);
Assert.Equal(QueryString.Empty, request.QueryString);
var query0 = request.Query;
Assert.NotNull(query0);
Assert.Equal(0, query0.Count);
requestFeature.QueryString = "?name0=value0&name1=value1";
var query1 = request.Query;
Assert.NotSame(query0, query1);
Assert.Equal(2, query1.Count);
Assert.Equal("value0", query1["name0"]);
Assert.Equal("value1", query1["name1"]);
var query2 = new ReadableStringCollection(new Dictionary<string, string[]>()
{
{ "name2", new[] { "value2" } }
});
request.Query = query2;
Assert.Same(query2, request.Query);
Assert.Equal("?name2=value2", requestFeature.QueryString);
Assert.Equal(new QueryString("?name2=value2"), request.QueryString);
}
[Fact]
public void Cookies_GetAndSet()
{
var request = new DefaultHttpContext().Request;
var cookieHeaders = request.Headers.GetValues("Cookie");
Assert.Null(cookieHeaders);
var cookies0 = request.Cookies;
Assert.Equal(0, cookies0.Count);
request.Headers.SetValues("Cookie", new[] { "name0=value0", "name1=value1" });
var cookies1 = request.Cookies;
Assert.Same(cookies0, cookies1);
Assert.Equal(2, cookies1.Count);
Assert.Equal("value0", cookies1["name0"]);
Assert.Equal("value1", cookies1["name1"]);
var cookies2 = new ReadableStringCollection(new Dictionary<string, string[]>()
{
{ "name2", new[] { "value2" } }
});
request.Cookies = cookies2;
Assert.Same(cookies2, request.Cookies);
Assert.Equal("value2", request.Cookies["name2"]);
cookieHeaders = request.Headers.GetValues("Cookie");
Assert.Equal(new[] { "name2=value2" }, cookieHeaders);
}
private static HttpRequest CreateRequest(IDictionary<string, string[]> headers)
{
var context = new DefaultHttpContext();