问题
Coming from C# and other languages, and new to F# and trying to port an SDK Library that I built in an OO language. The SDK is responsible for retrieving access token first, setting on a static field and then setting a specific interval to continuosly refresh the token before it's expiry.
The token is set as static field on the Authentication
class and is updated every time before it reaches expiry.
Then other actors within the SDK reach out to Authentication
class, read it's static
field token, place in their Authorization headers
before calling the REST endpoints. All actors within the SDK reuse the same token throughout for each call until it's expired and a newer one is auto fetched.
That's the behavior, I'm still trying to wrap my head around several concepts but I believe in learning while doing it.
This F# library will be called from C# where it will be passed Credentials to begin with and then subsequently instantiating other classes/actors and calling their methods while passing params for each individual method. Those other actors are the one who will be using this stored token.
The gist is basically having two static fields within the Authentication
and enable access to other actors while refreshing the one of the static fields i.e. Token.
public class Authentication
{
public static Token token;
public static Credentials credentials;
public static Token RequestToken(Credentials credentials)
{
Authentication.credentials = credentials // cache for subsequent use
// http REST call to access token based on credentials/api keys etc.
// Authentication.token = someAccessTokenObject; // cache result
}
public static Token AddTokenObserver(Credentials credentials)
{
this.RequestToken(credentials);
// set interval, like call RequestToken every 24 hrs
}
}
public class Class1
{
public someReturnObject RequestService1(someParams) {
// accesses Authentication.credentials
// accesses Authentication.token
// places in the authorization headers
// and calls the web service
}
// + several other methods that repeats the same pattern
}
public class Class2
{
public someReturnObject RequestService2(someParams) {
// accesses Authentication.credentials
// accesses Authentication.token
// places in the authorization headers
// and calls the web service
}
// + several other methods that repeats the same pattern
}
Use of SDK
// initialize SDK by passing credentials and enable auto refresh
Authentication.AddTokenObserver(someCredentials) // set token observer
Class1 c1 = new Class1();
c1.RequestService1(someObject1); // uses credentials & token from Authentication
Class c2 = new Class2();
c2.RequestService2(someObject2); // uses credentials & token from Authentication
My F# Attempt
type Credentials = {mutable clientId: string; mutable clientSecret: string;}
type Token = {mutable access_token: string; mutable refresh_token: string}
type Authentication =
static member token = {access_token = ""; refresh_token = ""};
static member credentials = {clientId = ""; clientSecret = "";}
new() = {}
member this.RequestToken(credentials) =
let data : byte[] = System.Text.Encoding.ASCII.GetBytes("");
let host = "https://example.com";
let url = sprintf "%s&client_id=%s&client_secret=%s" host credentials.clientId credentials.clientSecret
let request = WebRequest.Create(url) :?> HttpWebRequest
request.Method <- "POST"
request.ContentType <- "application/x-www-form-urlencoded"
request.Accept <- "application/json;charset=UTF-8"
request.ContentLength <- (int64)data.Length
use requestStream = request.GetRequestStream()
requestStream.Write(data, 0, (data.Length))
requestStream.Flush()
requestStream.Close()
let response = request.GetResponse() :?> HttpWebResponse
use reader = new StreamReader(response.GetResponseStream())
let output = reader.ReadToEnd()
printf "%A" response.StatusCode // if response.StatusCode = HttpStatusCode.OK throws an error
Authentication.credentials.clientId <- credentials.clientId
let t = JsonConvert.DeserializeObject<Token>(output)
Authentication.token.access_token <- t.access_token
Authentication.token.token_type <- t.token_type
reader.Close()
response.Close()
request.Abort()
F# Test
[<TestMethod>]
member this.TestCredentials() =
let credentials = {
clientId = "some client id";
clientSecret = "some client secret";
}
let authenticaiton = new Authentication()
try
authenticaiton.RequestToken(credentials)
printfn "%s, %s" credentials.clientId Authentication.credentials.clientId // Authentication.credentials.clientId is empty string
Assert.IsTrue(credentials.clientId = Authentication.credentials.clientId) // fails
with
| :? WebException -> printfn "error";
Question
In the above unit test
Authentication.credentials.clientId is empty string
Assert fails
I couldn't access static members within my unit tests after I called the token service. There's something wrong with how I'm approaching this all together.
I need help in translating C# behavior in F# with the help of some F# code. I've built the Authentication class and have some problems in the implementation especially around static members and subsequently accessing them. Also I want to follow the rules of functional programming and learn how it's done in Functional World in F#. Please help me translate this behavior in F# code.
回答1:
The idiomatic functional approach to this problem, would be to first try to get rid of the global state.
There are several approaches to this, but the one that I think will work best would be to provide an AuthenticationContext
which has the data your C# code keeps in global state, and make each call that migth renew the credentials, return its result together with a potentially updated authorization context.
Basically, given a method to make an API call with a token
type MakeApiCall<'Result> = Token -> 'Result
we want to create something like this:
type AuthenticatedCall<'Result> = AuthenticationContext -> 'Result * AuthenticationContext
You can then also let the context keep track of whether it needs renewal (e.g. by storing the timestamp when it was last renewed, storing the expiry date, or something else), and provide two functions
type NeedsRenewal = AuthenticationContext -> bool
type Renew = AuthenticationContext -> AuthenticationContext
Now, if you obtain the credentials with a function
type GetAccessToken = AuthenticationContext -> Token * AuthenticationContext
you can let the implementation of that method start by checking if the credentials need renewal, and if so renew them before returning.
So, a sample implementation might look like this:
type AuthenticationContext = {
credentials : Credentials
token : Token
expiryDate : DateTimeOffset
}
let needsRenewal context =
context.expiryDate > DateTimeOffset.UtcNow.AddMinutes(-5) // add some safety margin
let renew context =
let token = getNewToken context.Credentials
let expiryDate = DateTimeOffset.UtcNow.AddDays(1)
{ context with token = token, expiryDate = expiryDate }
let getAccessToken context =
let context' =
if needsRenewal context
then renew context
else context
return context'.token, context'
let makeAuthenticatedCall context makeApicall =
let token, context' = getAccessToken context
let result = makeApiCall token
result, context'
Now, if every time you make an API call you have access to the AuthenticationContext
from the previous call, the infrastructure will take care of renewing the token for you.
You'll notice quickly that this will just push the problem to keeping track of the authentication context, and that you will have to pass that around a lot. For example, if you want to make two consecutive API calls, you'll do the following:
let context = getInitialContext ()
let resultA, context' = makeFirstCall context
let resultB, context'' = makeSecondCall context'
Wouldn't it be nice if we could build something that would keep track of the context for us, so we didn't have to pass it around?
It turns out there's a functional pattern for this situation.
来源:https://stackoverflow.com/questions/49186245/token-access-reuse-and-auto-refresh-within-the-sdk