I'm trying to understand how to create a SignIn/SignUp service with ServiceStack and my database of choice is MongoDB:
public class AppHost : AppHostBase
{
public AppHost() : base("My Web Services", typeof(WelcomeService).Assembly) {}
public override void Configure(Container container)
{
Plugins.Add(new AuthFeature(() => new AuthUserSession(), new IAuthProvider[] {
new BasicAuthProvider()
}));
Plugins.Add(new RegistrationFeature());
var connectionString = ConfigurationManager.ConnectionStrings["mongodb"].ConnectionString;
var mongoClient = new MongoClient(connectionString);
var server = mongoClient.GetServer();
var db = server.GetDatabase("auth");
container.Register<ICacheClient>(new MemoryCacheClient());
container.Register<IUserAuthRepository>(new MongoDBAuthRepository(db, true));
}
The code above works correctly... it connects to the MongoDB server and creates the user table in the auth database. So far so good... What I'm trying to understand is how the built-in registration service works. If you look at my code, I enabled the RegistrationFeature but when I try to invoke it with http://localhost/register I always get a NotImplementedException. Does this mean I have to implement it from scratch? Is there any additional package to install? How do I actually invoke the default registration feature?
Related
I'm having some trouble getting claims from Google in a ASP.Net Core 2.1 Web API using OpenIdDict.
I've selected "No Authentication" in ASP.Net MVC template since I have no interest in storing usernames / passwords myself. I will be relying on external providers (such as Google) for authentication. The client is a SPA, so I'm using Implicit Flow.
My code is based on following this tutorial (except using Google rather than GitHub):
https://www.jerriepelser.com/blog/implementing-openiddict-authorization-server-part-2/
A Token is being returned from Google - but when I examine the JWT it does not contain any claims information.
What am I missing?
My Startup.cs looks like:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
// Register OpenIdDict database (EF Core)
services.AddDbContext<PD.Server.DataAccess.AuthorizationDbContext>(o =>
{
o.UseSqlServer(Configuration.GetConnectionString("AuthorizationDbContext"));
o.UseOpenIddict();
});
// Authentication
services.AddAuthentication(auth =>
{
auth.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
auth.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
auth.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
}).AddCookie()
.AddGoogle(o =>
{
o.ClientId = Configuration["Authentication:Google:ClientId";
o.ClientSecret = Configuration["Authentication:Google:ClientSecret"];
o.CallbackPath = "/signin-google";
});
services.AddOpenIddict()
.AddCore(o => o.UseEntityFrameworkCore().UseDbContext<AuthorizationDbContext>() )
.AddServer(o =>
{
o.UseMvc(); // Register MVC Binder
o.EnableAuthorizationEndpoint("/connect/authorize")
.EnableLogoutEndpoint("/connect/logout"); // Enable the Authorization end-point
o.RegisterScopes(OpenIddictConstants.Scopes.Email,
OpenIddictConstants.Scopes.Profile,
OpenIddictConstants.Scopes.Roles);
o.AllowImplicitFlow(); // Enable Implicit Flow (i.e. OAuth2 authentication for SPA's)
o.EnableRequestCaching();
o.DisableHttpsRequirement(); // DEV ONLY!
o.AddEphemeralSigningKey(); // DEV ONLY!
})
.AddValidation();
// Cors
services.AddCors();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseCors(builder =>
{
builder.WithOrigins("https://localhost:5001");
builder.WithMethods("GET");
builder.WithHeaders("Authorization");
});
app.UseAuthentication();
app.UseMvcWithDefaultRoute();
app.MigrateDatabase();
// Configure the OpenIdDict Database (not shown)
InitializeAsync(app.ApplicationServices, CancellationToken.None).GetAwaiter().GetResult();
}
And my Authenticate method:
[HttpGet("~/connect/authorize")]
public IActionResult Authorize(OpenIdConnectRequest request)
{
if (!User.Identity.IsAuthenticated) return Challenge("Google");
var claims = new List<Claim>();
claims.Add(new Claim(OpenIdConnectConstants.Claims.Subject, User.FindFirstValue(ClaimTypes.NameIdentifier), OpenIdConnectConstants.Destinations.IdentityToken));
claims.Add(new Claim(OpenIdConnectConstants.Claims.Name, User.FindFirstValue(ClaimTypes.Name), OpenIdConnectConstants.Destinations.IdentityToken));
claims.Add(new Claim(OpenIdConnectConstants.Claims.Email, User.FindFirstValue(ClaimTypes.Email), OpenIdConnectConstants.Destinations.IdentityToken));
claims.Add(new Claim(OpenIdConnectConstants.Claims.EmailVerified, "true", OpenIdConnectConstants.Destinations.IdentityToken));
var identity = new ClaimsIdentity(claims, "OpenIddict");
var principle = new ClaimsPrincipal(identity);
// Create a new Authentication Ticket
var ticket = new AuthenticationTicket(principle, new AuthenticationProperties(), OpenIdConnectServerDefaults.AuthenticationScheme);
return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme);
}
Thanks in advance.
Your claim destinations are not set correctly.
When using the Claim constructor that takes 3 parameter, it's the claim value type that is actually set, not the destination (which is a concept specific to OpenIddict).
Consider using the following syntax:
claims.Add(new Claim(OpenIdConnectConstants.Claims.Email, User.FindFirstValue(ClaimTypes.Email)).SetDestinations(OpenIdConnectConstants.Destinations.IdentityToken));
I have a remote tenant database application using MongoDB with authentication enabled. During run-time, I have to programmatically create a new tenant database, create a new tenant database user, create a new collection, and write to the new database with user metadata. My problem is that I am not setting up the tenant user authorization correctly. I am able to create the new tenant database and a database user with role credentials "readWrite". I also can correctly write the document to the "users" collection. If I use my admin credentials, I can access the Tenant database and examine the users document with no issue. However, if I try to later access the database with the newly created database user credentials I get an incorrect user credentials exception. Below is my code that creates the new tenant database,
MongoCredential adminCredentials = MongoCredential
.createCredential(adminuid, admindb, adminpw.toCharArray());
ServerAddress adminSA = new ServerAddress(mongoConnectionUri, mongoPort);
// Create the Mongo Password Vault Client & Document Template
MongoClient adminclient = new MongoClient(adminSA, Arrays.asList(adminCredtials)):
MongoClient adminclient MongoClient(adminSA,Arrays.asList(adminCredentials));
DB tenant = adminclient.getDB(tenantdb);
// Create pwvdb user
DBObject pwvdbrole = new BasicDBObject();
pwvdbrole.put("role", pwvrole);
pwvdbrole.put("db", pwvdb);
ArrayList<DBObject> pwvdbroles = new ArrayList<DBObject>();
pwvdbroles.add(pwvdbrole);
DBObject pwvaultcmd = new BasicDBObject();
pwvaultcmd.put("createUser", pwvuid);
pwvaultcmd.put("pwd", pwvpw);
pwvaultcmd.put("roles", pwvdbroles);
CommandResult result = tenant.command(pwvaultcmd);
if (result.ok()) {
System.out.println("Tenant Credentials: OK");
} else {
System.out.println("Tenant Credentials Error: " +
result.getErrorMessage());
}
// Create users Collection
DBCollection tenantCollection =
tenant.getCollection(tenantcollection);
// Create user default credentials & update user collection
BasicDBObject userdocument = new BasicDBObject();
userdocument.put("firstname", userfirstname);
userdocument.put("lastname", userlastname);
userdocument.put("email", useremail);
userdocument.put("username", username);
userdocument.put("password", pwencoder.encode(userpassword));
userdocument.put("role", Integer.parseInt(userrole));
// Create admin web portal users uid/pw
tenantCollection.insert(userdocument);
pwvclient.close();
Because the client is remote, I use my admin userid, pw, and db for the credentials. However, the MongoDB tenant client is setup using the new tenant database name. This is probably where I am going wrong but I don't know how to remotely access the database with a user I have not created yet. These databases are created at run time and I do not know the name of the tenant user when the application starts.
there are two different places for user permissions to be created, which can be confusing. Both can be done dynamically in your code.
User roles in tenant DB
The easiest option is for you to create the user you want in the tenant DB. You should use the runCommand call with the document structure for the createUser command documented here. You would create this user in the tenant DB. The role should be readWrite or any others documented.
Then later you would authenticate for the user and password for that same DB.
You can also centralize all of your user management in the Admin DB with each createUser call enumerating the roles for each DB/role pair you want. Let me know if you need that explanation. It works basically the same way to create users there and authenticate against that DB also. Then you switch to the DB you want to access on the same MongoClient.
I could not post 2 other helpful links because of strange limitation on posting more than 2 links if I am not an active enough poster (10 reputation).
Here is what I did to get it to work. The two key things I want to point out is the adminops and testops MongoDB templates. Notice that in adminops I used my MongoDB admin credentials but pointed to the testdb. In testops I used my new testdb credentials and point to testdb as well. My error was with adminops. I used the admindb which was incorrect. Also getting the roleobj and cmd was a little tricky but I eventially figured it out.
package com.belcan;
import java.util.ArrayList;
import java.util.Arrays;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration;
import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.MongoTemplate;
import com.mongodb.BasicDBObject;
import com.mongodb.CommandResult;
import com.mongodb.DBObject;
import com.mongodb.MongoClient;
import com.mongodb.MongoCredential;
import com.mongodb.ServerAddress;
#SpringBootApplication(exclude = {MongoAutoConfiguration.class, MongoDataAutoConfiguration.class})
public class PreConfigTestApplication implements CommandLineRunner {
public static void main(String[] args) {
// Fire off Spring Boot & Embedded Linux
SpringApplication.run(PreConfigTestApplication.class, args);
}
#Override
public void run(String...args) throws Exception {
String testdb = "testdb";
String testuid = "skm";
String testpw = "password";
// Set up credentials
MongoCredential admincredentials = MongoCredential
.createCredential(adminuid, admindb, adminpw.toCharArray());
ServerAddress adminsa = new ServerAddress(mongoConnectionUri, mongoPort);
// Create the Mongo Client & Document Template
MongoClient adminclient = new MongoClient(adminsa,Arrays.asList(admincredentials));
MongoOperations adminops = new MongoTemplate(adminclient, testdb);
// First create new user
DBObject roleobj = new BasicDBObject();
roleobj.put("role", "readWrite");
roleobj.put("db", "testdb");
ArrayList<DBObject> array = new ArrayList<DBObject>();
array.add(roleobj);
DBObject cmd = new BasicDBObject();
cmd.put("createUser", testuid);
cmd.put("pwd", testpw);
cmd.put("roles", array);
// Create new User
CommandResult result = (CommandResult) adminops.executeCommand(cmd);
// check to see if command is ok
if (result.ok()) {
System.out.println("createUser Command: OK");
} else {
System.out.println("createUser Error:" + result.getErrorMessage());
}
// switch to testdb
MongoOperations testops = new MongoTemplate(adminclient, testdb);
// Create the user object
User user = new User();
user.setFirstname("mickey");
user.setLastname("mouse");
user.setEmail("mmouse#gmail.com");
user.setUsername("mmouse");
user.setPassword("mmouse1");
user.setRole("USER");
user.setStatus("ACTIVE");
// Now save it
testops.save(user, "users");
// Close Database Connection
adminclient.close();
//
// Now see if we can open the file with the new uid/pw
//
// Set up credentials
MongoCredential testcredentials = MongoCredential
.createCredential(testuid, testdb, testpw.toCharArray());
ServerAddress testsa = new ServerAddress(mongoConnectionUri, mongoPort);
// Create the Mongo Client & Document Template
MongoClient testclient = new MongoClient(testsa,Arrays.asList(testcredentials));
MongoOperations myops = new MongoTemplate(testclient, testdb);
User myuser = new User();
myuser.setFirstname("Daffy");
myuser.setLastname("Duck");
myuser.setEmail("dduck#gmail.com");
myuser.setUsername("dduck");
myuser.setPassword("dduck1");
myuser.setRole("USER");
myuser.setStatus("ACTIVE");
// Now save it
myops.save(myuser,"users");
// Lets print it out
System.out.println("Print Results");
ArrayList<User> userout = (ArrayList<User>) myops.findAll(User.class, "users");
for (int i = 0; i < userout.size(); i++) {
System.out.println(userout.get(i));
}
System.out.println("\n\nAll Done....");
// Close the database
testclient.close();
}
// MongoDB Connection URI
#Value("${spring.data.mongodb.uri}")
private String mongoConnectionUri;
// MongoDB Connection Port
#Value("${spring.data.mongodb.port}")
private int mongoPort;
// MongoDB userid
#Value("${mongo.server.admin.db}")
private String admindb;
// MongoDB admin password
#Value("${mongo.server.admin.pw}")
private String adminpw;
// MongoDB admin userid
#Value("${mongo.server.admin.uid}")
private String adminuid;
}
Windows Azure Mobile Services currently doesn't have an option for custom authentication and looking at the feature request
http://feedback.azure.com/forums/216254-mobile-services/suggestions/3313778-custom-user-auth
It isn't coming anytime soon.
With a .NET backend and a .NET application how do you implement custom authentication, so that you don't have to use Facebook, Google or any of their other current providers?
There are plenty of partially completed tutorials on how this this is done with a JS backend and iOS and Android but where are the .NET examples?
I finally worked through the solution, with some help of the articles listed below, some intellisense and some trial and error.
How WAMS Works
First I wanted to describe what WAMS is in a very simple form as this part confused me for a while until it finally clicked. WAMS is just a collection of pre-existing technologies packaged up for rapid deployment. What you need to know for this scenario is:
As you can see WAMS is really just a container for a WebAPI and other things, which I won't go into detail here. When you create a new Mobile Service in Azure you get to download a project that contains the WebAPI. The example they use is the TodoItem, so you will see code for this scenario through the project.
Below is where you download this example from (I was just doing a Windows Phone 8 app)
I could go on further about this but this tutorial will get you started:
http://azure.microsoft.com/en-us/documentation/articles/mobile-services-dotnet-backend-windows-store-dotnet-get-started/
Setup WAMS Project
You will need your MasterKey and ApplicationKey. You can get them from the Azure Portal, clicking on your Mobile Services App and pressing Manage Keys at the bottom
The project you just downloaded, in the Controllers folder I just created a new controller called AccountController.cs and inside I put
public HttpResponseMessage GetLogin(String username, String password)
{
String masterKey = "[enter your master key here]";
bool isValidated = true;
if (isValidated)
return new HttpResponseMessage() { StatusCode = HttpStatusCode.OK, Content = new StringContent("{ 'UserId' : 'F907F58C-09FE-4F25-A26B-3248CD30F835', 'token' : '" + GetSecurityToken(new TimeSpan(1,0, 0), String.Empty, "F907F58C-09FE-4F25-A26B-3248CD30F835", masterKey) + "' }") };
else
return Request.CreateErrorResponse(HttpStatusCode.Unauthorized, "Username and password are incorrect");
}
private static string GetSecurityToken(TimeSpan periodBeforeExpires, string aud, string userId, string masterKey)
{
var now = DateTime.UtcNow;
var utc0 = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
var payload = new
{
exp = (int)now.Add(periodBeforeExpires).Subtract(utc0).TotalSeconds,
iss = "urn:microsoft:windows-azure:zumo",
ver = 2,
aud = "urn:microsoft:windows-azure:zumo",
uid = userId
};
var keyBytes = Encoding.UTF8.GetBytes(masterKey + "JWTSig");
var segments = new List<string>();
//kid changed to a string
var header = new { alg = "HS256", typ = "JWT", kid = "0" };
byte[] headerBytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(header, Formatting.None));
byte[] payloadBytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(payload, Formatting.None));
segments.Add(Base64UrlEncode(headerBytes));
segments.Add(Base64UrlEncode(payloadBytes));
var stringToSign = string.Join(".", segments.ToArray());
var bytesToSign = Encoding.UTF8.GetBytes(stringToSign);
SHA256Managed hash = new SHA256Managed();
byte[] signingBytes = hash.ComputeHash(keyBytes);
var sha = new HMACSHA256(signingBytes);
byte[] signature = sha.ComputeHash(bytesToSign);
segments.Add(Base64UrlEncode(signature));
return string.Join(".", segments.ToArray());
}
// from JWT spec
private static string Base64UrlEncode(byte[] input)
{
var output = Convert.ToBase64String(input);
output = output.Split('=')[0]; // Remove any trailing '='s
output = output.Replace('+', '-'); // 62nd char of encoding
output = output.Replace('/', '_'); // 63rd char of encoding
return output;
}
You can replace what is in GetLogin, with your own validation code. Once validated, it will return a security token (JWT) that is needed.
If you are testing on you localhost, remember to go into your web.config file and fill in the following keys
<add key="MS_MasterKey" value="Overridden by portal settings" />
<add key="MS_ApplicationKey" value="Overridden by portal settings" />
You need to enter in your Master and Application Keys here. They will be overridden when you upload them but they need to be entered if you are running everything locally.
At the top of the TodoItemController add the AuthorizeLevel attribute as shown below
[AuthorizeLevel(AuthorizationLevel.User)]
public class TodoItemController : TableController<TodoItem>
You will need to modify most of the functions in your TodoItemController but here is an example of the Get All function.
public IQueryable<TodoItem> GetAllTodoItems()
{
var currentUser = User as ServiceUser;
Guid id = new Guid(currentUser.Id);
return Query().Where(todo => todo.UserId == id);
}
Just a side note I am using UserId as Guid (uniqueidentifier) and you need to add this to the todo model definition. You can make the UserId as any type you want, e.g. Int32
Windows Phone/Store App
Please note that this is just an example and you should clean the code up in your main application once you have it working.
On your Client App
Install NuGet Package: Windows Azure Mobile Services
Go into App.xaml.cs and add this to the top
public static MobileServiceClient MobileService = new MobileServiceClient(
"http://localhost:50527/",
"[enter application key here]"
);
In the MainPage.xaml.cs I created
public class Token
{
public Guid UserId { get; set; }
public String token { get; set; }
}
In the main class add an Authenticate function
private bool Authenticate(String username, String password)
{
HttpClient client = new HttpClient();
// Enter your own localhost settings here
client.BaseAddress = new Uri("http://localhost:50527/");
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
HttpResponseMessage response = client.GetAsync(String.Format("api/Account/Login?username={0}&password={1}", username, password)).Result;
if (response.StatusCode == System.Net.HttpStatusCode.OK)
{
var token = Newtonsoft.Json.JsonConvert.DeserializeObject<Token>(response.Content.ReadAsStringAsync().Result);
App.MobileService.CurrentUser = new MobileServiceUser(token.UserId.ToString());
App.MobileService.CurrentUser.MobileServiceAuthenticationToken = token.token;
return true;
}
else
{
//Something has gone wrong, handle it here
return false;
}
}
Then in the Main_Loaded function
private void MainPage_Loaded(object sender, RoutedEventArgs e)
{
Authenticate("test", "test");
RefreshTodoItems();
}
If you have break points in the WebAPI, you will see it come in, get the token, then come back to the ToDoItemController and the currentUser will be filled with the UserId and token.
You will need to create your own login page as with this method you can't use the automatically created one with the other identity providers. However I much prefer creating my own login screen anyway.
Any other questions let me know in the comments and I will help if I can.
Security Note
Remember to use SSL.
References
[] http://www.thejoyofcode.com/Exploring_custom_identity_in_Mobile_Services_Day_12_.aspx
[] http://www.contentmaster.com/azure/creating-a-jwt-token-to-access-windows-azure-mobile-services/
[] http://chrisrisner.com/Custom-Authentication-with-Azure-Mobile-Services-and-LensRocket
This is exactly how you do it. This man needs 10 stars and a 5 crates of beer!
One thing, I used the mobile Service LoginResult for login like:
var token = Newtonsoft.Json.JsonConvert.DeserializeObject(response.Content.ReadAsStringAsync().Result);
Hope to get this into Android now!
I have a web application with an Angular / Breeze client side calling into a Breeze Web API, which uses an Entity Framework code first model. I have a datacontext (Angular service) responsible for all communications with server.
I would like to completely separate the server development from the client side development so developers need not even have .NET installed on their system. I would like the solution to require very little coding in way of creating fakes, because the app is changing frequently and I do not want to have to rewrite fakes every time my implementation changes. I have a bunch of test data in the database that I would like to make available on the client.
What is a good way (standard way?) to achieve this?
Just create mocks. You don't even have to make a RESTful call if you don't want to, just have your service decide whether to hit the server or pull from cache and load up your cache locally on start -
function loadMocks (manager) {
var personMockOne = manager.createEntity('Person', { id: 1, firstName: 'John', lastName: 'Smith' });
var companyMockOne = manager.createEntity('Company', { id: 1, name: 'Acme Inc.' });
companyMockOne.employees.push(personMockOne);
}
http://pwkad.wordpress.com/2014/02/02/creating-mocks-with-breeze-js/
To Expand...
Doing this requires a bit of extra set up. I personally always write my queries separate from my controller / view model logic through a service which takes parameters. A few example parameters are always something like parameters and forceRemote. The idea is that when you go to execute the query you can decide whether to hit the server or query locally. A quick example -
function queryHereOrThere (manager, parameters, forceRemote) {
var query = breeze.EntityQuery().from('EntityName').using(manager);
query.where(parameters);
if (!forceRemote) {
query.executeQueryLocally();
} else {
query.executeQuery();
}
}
Here is my current solution.
Get data from the server with a 'unit test' that creates a Breeze Web API controller and uses it to gather the breeze metadata and all the test data from the database, then writes that data to testData.json and breezeMetadata.json.
Abstract the creation of the Breeze Entity Manager to an Angular service entityManager.
Create a fakeEntityManager Angular service, which: 1) creates the entity manager, 2) overrides the EntityManager.executeQuery function to always use the local version, and 3) loads up the mgr with the test data. The code for that service is below.
In the datacontext service, use the $injector service to conditionally inject a real or a fake entity manager.
datacontext.js
angular.module('app').factory('datacontext', ['$injector','config', datacontext]);
function datacontext($injector, config) {
if (config.useLocalData === true) {
var mgr = $injector.get('fakeEntityManager');
} else var mgr = $injector.get('entityManager');
...
fakeEntityManager.js
(function() {
'use strict';
var serviceId = 'fakeEntityManager';
angular.module('app').factory(serviceId, ['breeze', 'common', em]);
function em(breeze, common) {
var $q = common.$q;
var mgr = getMgr();
populateManager(["Projects", "People", "Organizations"]);
return mgr;
function getMgr() {
breeze.EntityManager.prototype.executeQuery = function(query) {
return $q.when(this.executeQueryLocally(query)).then(function (results) {
var data = {
results: results
};
if (query.inlineCountEnabled == true) data.inlineCount = results.length;
return data;
});
};
var metaData = < PASTE JSON HERE >
new breeze.ValidationOptions({ validateOnAttach: false }).setAsDefault();
var metadataStore = new breeze.MetadataStore();
metadataStore.importMetadata(metaData, true);
return new breeze.EntityManager({
dataService: new breeze.DataService(
{
serviceName: "fakeApi",
hasServerMetadata: false // don't ask the server for metadata
}),
metadataStore: metadataStore
});
}
function populateManager(resources) {
var testData = < PASTE JSON HERE >;
resources.forEach(function (resource) {
testData[resource].forEach(function (entity) {
mgr.createEntity(mgr.metadataStore.getEntityTypeNameForResourceName(resource), entity);
});
});
}
}
})();
If you don't use inlineCount queries there is no need to override executeQuery. You can just add the following property to the EntityManager constructor's parameter:
queryOptions: new breeze.QueryOptions({ fetchStrategy: breeze.FetchStrategy.FromLocalCache })
Todo: Override the EntityManager.saveChanges() function (or somehow configure the entity manager) to prevent calls to the server while still allowing entities to be edited and saved locally.
I need to do some connectivity simulations to see that my code handles various connectivity errors to Facebook. I want to be able to simulate 500s, timeouts etc.
The easiest way to do that is to use Fiddler, but it seems to not be working with HTTPS (I get 403s when I try).
Is ther a way to force the SDK to work with HTTP instead of HTTPS for debugging purposes?
Facebook C# SDK supports your scenario for mocking the entire HttpWebRequest and HttpWebResponse. In fact we actually use that internally in our unit tests so that every single line of the code in Facebook C# SDK actually gets executed and the result is always the same. https://github.com/facebook-csharp-sdk/facebook-csharp-sdk/blob/v5/Source/Facebook.Tests/TestExtensions.cs For now you will need to check these tests in v5 branch as we haven't yet migrated those tests to v6.
For v5, you will need to override the protected CreateHttpWebRequest method in FacebookClient.
Here is an example for v5 when there is no internet connection. There are three hidden classes HttpWebRequestWrapper, HttpWebResponseWrapper and WebExceptionWrapper that you will need to make use of.
public static void NoInternetConnection(this Mock<Facebook.FacebookClient> facebookClient, out Mock<HttpWebRequestWrapper> mockRequest, out Mock<WebExceptionWrapper> mockWebException)
{
mockRequest = new Mock<HttpWebRequestWrapper>();
mockWebException = new Mock<WebExceptionWrapper>();
var mockAsyncResult = new Mock<IAsyncResult>();
var request = mockRequest.Object;
var webException = mockWebException.Object;
var asyncResult = mockAsyncResult.Object;
mockRequest.SetupProperty(r => r.Method);
mockRequest.SetupProperty(r => r.ContentType);
mockRequest.SetupProperty(r => r.ContentLength);
mockAsyncResult
.Setup(ar => ar.AsyncWaitHandle)
.Returns((ManualResetEvent)null);
mockWebException
.Setup(e => e.GetResponse())
.Returns<HttpWebResponseWrapper>(null);
mockRequest
.Setup(r => r.GetResponse())
.Throws(webException);
mockRequest
.Setup(r => r.EndGetResponse(It.IsAny<IAsyncResult>()))
.Throws(webException);
AsyncCallback callback = null;
mockRequest
.Setup(r => r.BeginGetResponse(It.IsAny<AsyncCallback>(), It.IsAny<object>()))
.Callback<AsyncCallback, object>((c, s) =>
{
callback = c;
})
.Returns(() =>
{
callback(asyncResult);
return asyncResult;
});
var mockRequestCopy = mockRequest;
var mockWebExceptionCopy = mockWebException;
facebookClient.Protected()
.Setup<HttpWebRequestWrapper>("CreateHttpWebRequest", ItExpr.IsAny<Uri>())
.Callback<Uri>(uri =>
{
mockRequestCopy.Setup(r => r.RequestUri).Returns(uri);
mockWebExceptionCopy.Setup(e => e.Message).Returns(string.Format("The remote name could not be resolved: '{0}'", uri.Host));
})
.Returns(request);
}
You can then write your tests as below.
[Fact]
public void SyncWhenThereIsNotInternetConnectionAndFiddlerIsNotOpen_ThrowsWebExceptionWrapper()
{
var mockFb = new Mock<FacebookClient> { CallBase = true };
Mock<HttpWebRequestWrapper> mockRequest;
Mock<WebExceptionWrapper> mockWebException;
mockFb.NoInternetConnection(out mockRequest, out mockWebException);
Exception exception = null;
try
{
var fb = mockFb.Object;
fb.Get(_parameters);
}
catch (Exception ex)
{
exception = ex;
}
mockFb.VerifyCreateHttpWebRequest(Times.Once());
mockRequest.VerifyGetResponse();
mockWebException.VerifyGetReponse();
Assert.IsAssignableFrom<WebExceptionWrapper>(exception);
}
In v6 we have made mocking the HttpWebRequest and HttpWebResponse much easier.
Create your custom HttpWebRequest and HttpWebResponse by inheriting HttpWebRequestWrapper and HttpWebReponseWrapper.
Then change the default http web request factory for Facebook C# SDK. Here is the sample of the default factory.
FacebookClient.SetDefaultHttpWebRequestFactory(uri => new HttpWebRequestWrapper((HttpWebRequest)WebRequest.Create(uri)));
If you want to change the HttpWebRequestFactor per FacebookClient instance then use the following code.
var fb = new FacebookClient();
fb.HttpWebRequestFactory = uri=> new MyHttpWebRequestWrapper(uri);
Note: HttpWebRequestWrapper, HttpWebResponseWrapper, WebExceptionWrapper, FacebookClient.SetDefaultHttpWebRequestFactory and FacebookClient.HttpWebRequestFactory has the attribute [EditorBrowsable(EditorBrowsableState.Never)] so you might not see it in the intellisense.
Things like no internet connection that you mention should actually be a part of facebook c# sdk tests and not your app unit tests. The sdk should guarantee that when there is not internet conenction it always throws WebExceptionWrapper and your app unit tests should actually be handling the WebExceptionWrapper exception and not mocking the entire httpwebrequest and httpwebresponse.
I'd suggest you introduce another level of abstraction to your code and code to that abstraction rather than the implementation. Eg.
public interface IFacebookClient {
IEnumerable<Friend> GetFriends();
}
public class HttpsClient : IFacebookClient {
public IEnumerable<Friend> GetFriends() {
// Make a call out to the Facebook API, as per usual
};
}
In your consuming code you'd do something like;
public class ConsumingCode {
private IFacebookClient _client;
public ConsumingCode(IFacebookClient client) {
_client = client;
foreach (Friend friend in _client.GetFriends()) {
// Do something with each Friend
}
}
}
If you're using an IoC container this can all get wired up for you automatically. MVVM frameworks like Caliburn.Micro tend to support this as well.
Then when it comes to unit testing (or manual testing) you can change the implementation of your interface;
public class Http403Client : IFacebookClient {
public IEnumerable<Friend> GetFriends() {
throw new HttpException(403, "Forbidden");
}
}
Obviously this is just a mock up example but I think it demonstrates the concept that you want to implement.