I am using MVVM Prism for the Xamarin mobile app.
I created a NetCore 3.1 rest api that serves as the backend to my Xamarin mobile app. I want to be able to debug it locally.
I would like to:
A. Connect to localhost to debug the rest api from the Simulators
B. Also would like to connect to the localhost kestrel from the mobile app itself in debug mode if possible
I am using NetCore Kestrel instead of IIS to host the rest api in the localhost.
I can connect with Postman locally and DEBUG ok in VS using this URL:
even though I do get a first certificate validation error, but I turned off Certificate validation in POSTMAN to make it work, but when I try to connect to that same URL from the mobile, with the code below I get this error:
I have been struggling with this for 2 full days, this is my code:
REST API PROJECT
Program.cs class:
namespace MyAppNamespace
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureServices((context, services) =>
{
services.Configure<KestrelServerOptions>(
context.Configuration.GetSection("Kestrel"));
})
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.ConfigureKestrel(serverOptions =>
{
// Set properties and call methods on options
serverOptions.Limits.MaxConcurrentConnections = 100;
serverOptions.Limits.MaxConcurrentUpgradedConnections = 100;
serverOptions.Limits.MaxRequestBodySize = 10 * 1024;
serverOptions.Limits.MinRequestBodyDataRate =
new MinDataRate(bytesPerSecond: 100,
gracePeriod: TimeSpan.FromSeconds(10));
serverOptions.Limits.MinResponseDataRate =
new MinDataRate(bytesPerSecond: 100,
gracePeriod: TimeSpan.FromSeconds(10));
serverOptions.Listen(IPAddress.Loopback, 5000);
serverOptions.Listen(IPAddress.Loopback, 5001,
listenOptions =>
{
listenOptions.UseHttps(",myapphosting.pfx",
"mypassword"); // I did not specify where these files are in the code, it seems to just know?
});
serverOptions.Limits.KeepAliveTimeout =
TimeSpan.FromMinutes(2);
serverOptions.Limits.RequestHeadersTimeout =
TimeSpan.FromMinutes(1);
})
.UseStartup<Startup>();
});
}
}
Startup class:
namespace MyAppNamespace
{
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)
{
services.AddControllers();
services.AddMvc(option => option.EnableEndpointRouting = false)
.SetCompatibilityVersion(CompatibilityVersion.Version_3_0)
.AddNewtonsoftJson(opt => opt.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore);
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = Configuration["Tokens:Issuer"],
ValidAudience = Configuration["Tokens:Issuer"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Tokens:Key"])),
ClockSkew = TimeSpan.Zero,
};
});
services.AddDbContext<MyAppDbContext>(option => option.UseSqlServer("MyConnectionString"));
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, MyDbContext myDbContext)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
//else
//{
// app.UseHsts();
//}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
myDbContext.Database.EnsureCreated();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
}
launchSettings.Json :
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iis": {
"applicationUrl": "http://localhost/MyWebApiName",
"sslPort": 0
},
"iisExpress": {
"applicationUrl": "http://localhost:52080",
"sslPort": 44349
}
},
"$schema": "http://json.schemastore.org/launchsettings.json",
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "api/accounts/register",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"YWAW.WebApi": {
"commandName": "Project",
"launchBrowser": true,
"launchUrl": "api/accounts/register",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:5001;http://localhost:5000"
}
}
}
I followed this tutorial to set up the to bypass the Bypass the certificate security check and grab an insecurehandler on both Android and iOS platforms.
https://docs.microsoft.com/en-us/xamarin/cross-platform/deploy-test/connect-to-local-web-services
XAMARIN FORMS PROJECT
in Constants class(shared project):
public static string BaseAddress =
Device.RuntimePlatform == Device.Android ? "https://10.0.2.2:5001" : "https://localhost:5001"; // here I tried writing the iOS url as in Postman above too, same result
public static string RestUrl = $"{BaseAddress}/api/Accounts/Register";
App.Xaml.cs
public partial class App
{
/*
* The Xamarin Forms XAML Previewer in Visual Studio uses System.Activator.CreateInstance.
* This imposes a limitation in which the App class must have a default constructor.
* App(IPlatformInitializer initializer = null) cannot be handled by the Activator.
*/
public App() : this(null) { }
public App(IPlatformInitializer initializer) : base(initializer) { }
protected override async void OnInitialized()
{
InitializeComponent();
await NavigationService.NavigateAsync("NavigationPage/HomePage");
}
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
containerRegistry.RegisterSingleton<IAppInfo, AppInfoImplementation>();
containerRegistry.RegisterForNavigation<NavigationPage>();
containerRegistry.RegisterForNavigation<MainPage, MainPageViewModel>();
containerRegistry.RegisterForNavigation<HomePage, HomePageViewModel>();
containerRegistry.RegisterForNavigation<LoginPage, LoginPageViewModel>();
containerRegistry.RegisterForNavigation<SignUpPage, SignUpPageViewModel>();
containerRegistry.RegisterForNavigation<DefaultCustomActivityIndicatorPage, DefaultCustomActivityIndicatorPageViewModel>();
containerRegistry.RegisterForNavigation<DiscoverDealsTabbedPage, DiscoverDealsTabbedPageViewModel>();
containerRegistry.Register<ApiServices>(); //not sure if I actually should register this too
}
// add for app center analytics and crashes
protected override void OnStart()
{
base.OnStart();
AppCenter.Start("ios=secretkey" +
"uwp={Your UWP App secret here};" +
"android={Your Android App secret here}",
typeof(Analytics), typeof(Crashes));
}
}
LoginPageViewModel.cs
public class LoginPageViewModel : BindableBase
{
private IPageDialogService _dialogService { get; }
private ApiServices _apiService { get; }
public DelegateCommand SignInWithEmailTappedCmd { get; set; }
public DelegateCommand SignInWithFacebookTappedCmd { get; set; }
public DelegateCommand SignInWithGoogleTappedCmd { get; set; }
private IFacebookClient _facebookService = CrossFacebookClient.Current;
private IGoogleClientManager _googleService = CrossGoogleClient.Current;
public INavigationService NavigationService { get; set; }
private readonly ICustomActivityIndicatorPage _customActivityIndicator;
private string _emailAddress;
public string EmailAddress
{
get => _emailAddress;
set => SetProperty(ref _emailAddress, value);
}
private string _password;
public string Password
{
get => _password;
set => SetProperty(ref _password, value);
}
public LoginPageViewModel(ICustomActivityIndicatorPage customActivityIndicator,
IHttpClientHandlerService httpClientHandlerService,
INavigationService navigationService,
IPageDialogService dialogService)
{
_customActivityIndicator = customActivityIndicator;
SignInWithEmailTappedCmd = new DelegateCommand(SignInWithEmailTapped);
SignInWithFacebookTappedCmd = new DelegateCommand(async() => await SignInWithFacebookTapped());
SignInWithGoogleTappedCmd = new DelegateCommand(async() => await SingInWithGoogleTapped());
NavigationService = navigationService;
_dialogService = dialogService;
_apiService = new ApiServices(httpClientHandlerService);
}
/// <summary>
///
/// </summary>
/// <returns></returns>
private async Task SignInWithFacebookTapped()
{
try
{
if (_facebookService.IsLoggedIn)
_facebookService.Logout();
EventHandler<FBEventArgs<string>> userDataDelegate = null;
_customActivityIndicator.InitActivityPage(new DefaultCustomActivityIndicatorPage());
_customActivityIndicator.ShowActivityPage();
userDataDelegate = async (object sender, FBEventArgs<string> e) =>
{
switch (e.Status)
{
case FacebookActionStatus.Completed:
var facebookProfile = await Task.Run(() => JsonConvert.DeserializeObject<FacebookProfile>(e.Data));
// save the user to the db if doesn't exist
UserToRegister user = new UserToRegister
{
Email = facebookProfile.Email,
FirstName = facebookProfile.FirstName,
LastName = facebookProfile.LastName
};
// THIS IS WHERE I TRY TO ACCESS THE LOCALHOST REST API
**var registerOutcome = await _apiService.Register(user);**
await NavigationService.NavigateAsync("DiscoverDealsTabbedPage");
_customActivityIndicator.HideActivityPage();
break;
case FacebookActionStatus.Canceled:
_customActivityIndicator.HideActivityPage();
await _dialogService.DisplayAlertAsync("Facebook Auth", "Canceled", "Ok");
break;
case FacebookActionStatus.Error:
_customActivityIndicator.HideActivityPage();
await _dialogService.DisplayAlertAsync("Facebook Auth", "Error", "Ok");
break;
case FacebookActionStatus.Unauthorized:
_customActivityIndicator.HideActivityPage();
await _dialogService.DisplayAlertAsync("Facebook Auth", "Unauthorized", "Ok");
break;
}
_facebookService.OnUserData -= userDataDelegate;
};
_facebookService.OnUserData += userDataDelegate;
string[] fbRequestFields = { "email", "first_name", "picture", "gender", "last_name" };
string[] fbPermisions = { "email" };
await _facebookService.RequestUserDataAsync(fbRequestFields, fbPermisions);
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}
finally
{
_customActivityIndicator.HideActivityPage();
}
}
}
ApiServices class:
public class ApiServices
{
private HttpClient httpClient;
private string apiRegisterURL;
private readonly IHttpClientHandlerService _httpClientHandler;
public ApiServices(IHttpClientHandlerService httpClientHandler)
{
#if DEBUG
_httpClientHandler = httpClientHandler;
httpClient = new HttpClient(_httpClientHandler.GetInsecureHandler());
apiRegisterURL = Constants.RestUrl;
#else
httpClient = new HttpClient();
apiRegisterURL = Constants.REGISTER_URL;
#endif
}
/// <summary>
///
/// </summary>
/// <param name="user"></param>
/// <returns></returns>
public async Task<RegisterOutcome> Register(UserToRegister user)
{
RegisterOutcome outcome = new RegisterOutcome();
//var httpClient = new HttpClient();
try
{
var json = JsonConvert.SerializeObject(user);
var content = new StringContent(json, Encoding.UTF8, "application/json");
outcome.ResponseMessage = await httpClient.PostAsync(apiRegisterURL, content);
}
catch (Exception ex) // ERROR IS HERE CONNECTION REFUSED
{
outcome.ErrorMessage = ex.Message;
}
return outcome;
}
}
IOS PROJECT:
iOSHttpClientHandlerService.cs:
[assembly: Dependency(typeof(iOSHttpClientHandlerService))]
namespace YWAWMobileApp.iOS.Services
{
public class iOSHttpClientHandlerService : IHttpClientHandlerService
{
public HttpClientHandler GetInsecureHandler()
{
HttpClientHandler handler = new HttpClientHandler();
handler.ServerCertificateCustomValidationCallback = (message, cert, chain, errors) =>
{
if (cert.Issuer.Equals("CN=localhost"))
return true;
return errors == System.Net.Security.SslPolicyErrors.None;
};
return handler;
}
}
}
AppDelegate.cs:
public class iOSInitializer : IPlatformInitializer
{
public void RegisterTypes(IContainerRegistry containerRegistry)
{
// Register any platform specific implementations
containerRegistry.Register<ICustomActivityIndicatorPage, iOSCustomActivityIndicatorPage>();
containerRegistry.Register<IHttpClientHandlerService, iOSHttpClientHandlerService>();
}
}
ANDROID project same as IOS.