Accessing Dropbox from Xamarin Forms using PKCE OAuth and .NET API - solution - xamarin.forms

Implementing Dropbox support in Xamarin Forms was, let’s say, interesting, especially using the more secure PKCE OAuth flow, which requires deep linking, as WebView is insecure.
For anyone struggling as much as I was, working code is shown below, including shared code and Android code. I haven’t needed to implement the iOS side as I’m using iCloud rather than Dropbox there, but that should be straightforward.
You may want to add an ActivityIndicator to the calling page, as it pops in and out of view during authorization.
Note: While the Dropbox .NET API is not officially supported for Xamarin, it can be made to work, as shown here.
EDIT 18 Sep 2021:
Added code to (1) handle case where user declines to accept access to Dropbox and (2) close the browser after authorization. A remaining issue: each time we authorize, a tab gets added to the browser - don't see how to overcome that.

ANDROID CODE
using System;
using System.Net;
using System.Threading.Tasks;
using Xamarin.Forms;
using Android.Content;
using Android.App;
using Plugin.CurrentActivity;
using MyApp.Droid.DropboxAuth;
using AndroidX.Activity;
[assembly: Dependency (typeof (DropboxOAuth2_Android))]
namespace MyApp.Droid.DropboxAuth
{
public class DropboxOAuth2_Android: Activity, IDropbox
{
public bool IsBrowserInstalled ()
// Returns true if a web browser is installed
{
string url = "https://google.com"; // Any url will do
Android.Net.Uri webAddress = Android.Net.Uri.Parse ( url );
Intent intentWeb = new Intent ( Intent.ActionView, webAddress );
Context currentContext = CrossCurrentActivity.Current.Activity;
Android.Content.PM.PackageManager packageManager = currentContext.PackageManager;
return intentWeb.ResolveActivity ( packageManager ) != null;
}
public void OpenBrowser ( string url )
// Opens default browser
{
Intent intent = new Intent ( Intent.ActionView, Android.Net.Uri.Parse ( url ) );
Context currentContext = CrossCurrentActivity.Current.Activity;
currentContext.StartActivity ( intent );
}
public void CloseBrowser ()
// Close the browser
{
Finish ();
}
}
}
using System;
using Android.App;
using Android.Content;
using Android.OS;
using Android.Content.PM;
using MyApp.DropboxService;
namespace MyApp.Droid.DropboxAuth
{
public class Redirection_Android
{
[Activity(NoHistory = true, LaunchMode = LaunchMode.SingleTop)]
[IntentFilter ( new [] { Intent.ActionView },
Categories = new[] { Intent.CategoryBrowsable, Intent.CategoryDefault },
DataScheme = "com.mydomain.myapp" )]
public class RedirectHandler : Activity
{
protected async override void OnCreate ( Bundle savedInstanceState )
{
base.OnCreate( savedInstanceState );
Intent intent = Intent; // The intent that started this activity
if ( Intent.Action == Intent.ActionView )
{
Android.Net.Uri uri = intent.Data;
if ( uri.ToString ().Contains ("The+user+chose+not+to+give+your+app+access" ) )
{
// User pressed Cancel not Accept
if ( MyApp.DropboxService.Authorization.Semaphore != null )
{
// Release semaphore
Behayve.DropboxService.Authorization.Semaphore.Release ();
Behayve.DropboxService.Authorization.Semaphore.Dispose ();
Behayve.DropboxService.Authorization.Semaphore = null;
}
Xamarin.Forms.DependencyService.Get<IDropbox> ().CloseBrowser ();
Finish ();
return;
}
if ( uri.GetQueryParameter ( "state" ) != null )
{
// Protect from curious eyes
if ( uri.GetQueryParameter ( "state" ) != Authorization.StatePKCE )
Finish ();
if ( uri.GetQueryParameter ( "code" ) != null )
{
string code = uri.GetQueryParameter ( "code" );
// Perform stage 2 flow, storing tokens in settings
bool success = await Authorization.Stage2FlowAsync ( code );
Authorization.IsAuthorizationComplete = true;
// Allow shared code that initiated this activity to continue
Authorization.Semaphore.Release ();
}
}
}
Finish ();
}
}
}
}
NOTE: If targeting API 30 or later, add the following to your manifest within the <queries> tag:
<intent>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
</intent>
SHARED CODE
using System;
namespace MyApp
{
public interface IDropbox
{
bool IsBrowserInstalled (); // True if a browser is installed
void OpenBrowser ( string url ); // Opens url in internal browser
void CloseBrowser (); // Closes the browser
}
}
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Net.Http;
using Xamarin.Forms;
using MyApp.Resx;
using Dropbox.Api;
namespace MyApp.DropboxService
{
public class Authorization
{
private const string packageName = "com.mydomain.myapp"; // Copied from Android manifest
private const string redirectUri = packageName + ":/oauth2redirect";
private static PKCEOAuthFlow pkce;
private const string clientId = “abcabcabcabcabc”; // From Dropbox app console
private static DropboxClientConfig dropboxClientConfig;
// Settings keys
private const string accessTokenKey = "accessTokenKey";
public const string refreshTokenKey = "refreshTokenKey";
private const string userIdKey = "userIdKey";
public static string StatePKCE {get; private set; }
public static SemaphoreSlim Semaphore { get; set; } // Allows shared code to wait for redirect-triggered Android activity to complete
public static volatile bool IsAuthorizationComplete; // Authorization is complete, tokens stored in settings
public Authorization ()
{
IsAuthorizationComplete = false;
Semaphore = new SemaphoreSlim ( 1,1 );
}
public async Task<DropboxClient> GetAuthorizedDropBoxClientAsync ()
// If access tokens not already stored in secure settings, first verifies a browser is installed,
// then after a browser-based user authorisation dialog, securely stores access token, refresh token and user ID in settings.
// Returns a long-lived authorised DropboxClient (based on a refresh token stored in settings).
// Returns null if not authorised or no browser or if user hit Cancel or Back (no token stored).
// Operations can then be performed on user's Dropbox over time via the DropboxClient.
//
// Assumes caller has verified Internet is available.
//
// Employs the PKCE OAuth flow.
// WebView is not used because of associated security issues -- deep linking is used instead.
// The tokens can be retrieved from settings any time should they be desired.
// No auxiliary website is used.
{
if ( string.IsNullOrEmpty ( await Utility.GetSettingAsync ( refreshTokenKey ) ) )
{
// We do not yet have a refresh key
try
{
// Verify user has a suitable browser installed
if ( ! DependencyService.Get<IDropbox> ().IsBrowserInstalled () )
{
await App.NavPage.DisplayAlert ( T.NoBrowserInstalled, T.InstallBrowser, T.ButtonOK );
return null;
}
// Stage 1 flow
IsAuthorizationComplete = false;
DropboxCertHelper.InitializeCertPinning ();
pkce = new PKCEOAuthFlow (); // Generates code verifier and code challenge for PKCE
StatePKCE = Guid.NewGuid ().ToString ( "N" );
// NOTE: Here authorizeRedirectUI is of the form com.mydomain.myapp:/oauth2redirect
Uri authorizeUri = pkce.GetAuthorizeUri ( OAuthResponseType.Code, clientId: clientId, redirectUri:redirectUri,
state: StatePKCE, tokenAccessType: TokenAccessType.Offline, scopeList: null, includeGrantedScopes: IncludeGrantedScopes.None );
// NOTE: authorizeUri looks like this:
// https://www.dropbox.com/oauth2/authorize?response_type=code&client_id=abcabcabcabcabc&redirect_uri=com.mydomain.myapp%3A%2Foauth2redirect&state=51cbbd2b7bce4d7990bc72fc95991375&token_access_type=offline&code_challenge_method=S256&code_challenge=r75HUStz-F43vWl2yr9m5ctgF1lgE7uqu-cf_gQpSEU
// Open authorization url in browser
await Semaphore.WaitAsync (); // Take semaphore
DependencyService.Get<IDropbox> ().OpenBrowser ( authorizeUri.AbsoluteUri );
// Wait until Android redirection activity obtains tokens and releases semaphore
// NOTE: User might first press Cancel or Back button - this returns user to page calling this method, where OnAppearing will run
await Semaphore.WaitAsync ();
}
catch
{
if ( Semaphore != null )
Semaphore.Dispose ();
return null;
}
}
else
IsAuthorizationComplete = true;
// Wrap up
if ( Semaphore != null )
Semaphore.Dispose ();
if ( IsAuthorizationComplete )
{
// Return authorised Dropbox client
DropboxClient dropboxClient = await AuthorizedDropboxClientAsync ();
DependencyService.Get<IDropbox> ().CloseBrowser ();
return dropboxClient;
}
return null;
}
public static async Task<bool> Stage2FlowAsync ( string code )
// Obtains authorization token, refresh token and user Id, and
// stores them in settings.
// code = authorization code obtained in stage 1 flow
// Returns true if tokens obtained
{
// Retrieve tokens
OAuth2Response response = await pkce.ProcessCodeFlowAsync ( code, clientId, redirectUri: redirectUri );
if ( response == null )
return false;
string accessToken = response.AccessToken;
string refreshToken = response.RefreshToken;
string userId = response.Uid;
// Save tokens in settings
await Utility.SetSettingAsync ( accessTokenKey, accessToken );
await Utility.SetSettingAsync ( refreshTokenKey, refreshToken );
await Utility.SetSettingAsync ( userIdKey, userId );
return true;
}
public static async Task<DropboxClient> AuthorizedDropboxClientAsync ( )
// Returns authorized Dropbox client, or null if none available
// For use when Dropbox authorization has already taken place
{
string refreshToken = await Utility.GetSettingAsync ( Authorization.refreshTokenKey );
// NOTE: Due to Dropbox.NET API bug for Xamarin, we need to override Android Build HttpClientImplementation setting (AndroidClientHandler) with HTTPClientHandler, for downloads to work
dropboxClientConfig = new DropboxClientConfig () { HttpClient = new HttpClient ( new HttpClientHandler () ) };
return new DropboxClient ( refreshToken, clientId, dropboxClientConfig );
}
public static async Task ClearTokensInSettingsAsync ()
// Clears access token, refresh token, user Id token
// Called when app initialises
{
await Utility.SetSettingAsync ( accessTokenKey, string.Empty );
await Utility.SetSettingAsync ( refreshTokenKey, string.Empty );
await Utility.SetSettingAsync ( userIdKey, string.Empty );
}
public static async Task<bool> IsLoggedInAsync ()
// Returns true if logged in to Dropbox
{
if ( await Utility.GetSettingAsync ( refreshTokenKey ) == string.Empty )
return false;
return true;
}
}
}
using System;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.IO;
using System.Text;
using Dropbox.Api;
using Dropbox.Api.Files;
using MyApp.Resx;
namespace MyApp.DropboxService
{
public class FileHelper
{
const string _FNF = “~FNF”;
public static async Task<bool> ExistsAsync ( DropboxClient dbx, string path )
// Returns true if given filepath/folderpath exists for given Dropbox client
// Dropbox requires "/" to be the initial character
{
try
{
GetMetadataArg getMetadataArg = new GetMetadataArg ( path );
Metadata xx = await dbx.Files.GetMetadataAsync ( getMetadataArg );
}
catch ( Exception ex )
{
if ( ex.Message.Contains ( "not_found" ) ) // Seems no other way to do it
return false;
await Utility.WriteLogFileAsync ( T.Exception, ex.AsString () );
throw new Exception ( "In FileHelper.ExistsAsync " + ex.ToString (), ex.InnerException );
}
return true;
}
public static async Task<CreateFolderResult> CreateFolderAsync ( DropboxClient dbx, string path )
// Creates folder for given Dropbox user at given path, unless it already exists
// Returns CreateFolderResult, or null if already exists
{
try
{
if ( await ExistsAsync ( dbx, path ) )
return null;
CreateFolderArg folderArg = new CreateFolderArg( path );
return await dbx.Files.CreateFolderV2Async( folderArg );
}
catch ( Exception ex )
{
await Utility.WriteLogFileAsync ( T.Exception, ex.AsString () );
throw new Exception ( "In FileHelper.CreateFolderAsync " + ex.ToString (), ex.InnerException );
}
}
public static async Task DeleteFileAsync ( DropboxClient dbx, string path )
// Delete given Dropbox user's given file
{
try
{
DeleteArg deleteArg = new DeleteArg ( path );
await dbx.Files.DeleteV2Async ( deleteArg );
}
catch ( Exception ex )
{
await Utility.WriteLogFileAsync ( T.Exception, ex.AsString () );
throw new Exception ( "In FileHelper.DeleteFileAsync " + ex.ToString (), ex.InnerException );
}
}
public static async Task<FileMetadata> UploadBinaryFileAsync ( DropboxClient dbx, string localFilepath, string dropboxFilepath )
// Copies given local binary file to given Dropbox file, deleting any pre-existing destination file
// NOTE: Dropbox requires initial "/" in dropboxFilePath
{
int tries = 0;
while ( tries < 30 )
{
try
{
if ( await ExistsAsync ( dbx, dropboxFilepath ) )
await DeleteFileAsync ( dbx, dropboxFilepath );
using ( FileStream localStream = new FileStream ( localFilepath, FileMode.Open, FileAccess.Read ) )
{
return await dbx.Files.UploadAsync ( dropboxFilepath,
WriteMode.Overwrite.Instance,
body: localStream );
}
}
catch ( RateLimitException ex )
{
// We have to back off and retry later
int backoffSeconds= ex.RetryAfter; // >= 0
System.Diagnostics.Debug.WriteLine ( "****** Dropbox requested backoff of " + backoffSeconds.ToString () + " seconds" );
await Task.Delay ( backoffSeconds * 1000 );
}
catch ( Exception ex )
{
await Utility.WriteLogFileAsync ( T.Exception, ex.AsString () );
throw new Exception ( "In FileHelper.UploadBinaryFileAsync " + ex.ToString (), ex.InnerException );
}
tries++;
}
return null;
}
public static async Task<FileMetadata> UploadTextFileAsync ( DropboxClient dbx, string localFilepath, string dropboxFilepath )
// Copies given local text file to given Dropbox file, deleting any pre-existing destination file
{
int tries = 0;
while ( tries < 30 )
{
try
{
if ( await ExistsAsync ( dbx, dropboxFilepath ) )
await DeleteFileAsync ( dbx, dropboxFilepath );
string fileContents = File.ReadAllText ( localFilepath );
using ( MemoryStream localStream = new MemoryStream ( Encoding.UTF8.GetBytes ( fileContents ) ) )
{
return await dbx.Files.UploadAsync ( dropboxFilepath,
WriteMode.Overwrite.Instance,
body: localStream );
}
}
catch ( RateLimitException ex )
{
// We have to back off and retry later
int backoffSeconds= ex.RetryAfter; // >= 0
System.Diagnostics.Debug.WriteLine ( "****** Dropbox requested backoff of " + backoffSeconds.ToString () + " seconds" );
await Task.Delay ( backoffSeconds * 1000 );
}
catch ( Exception ex )
{
await Utility.WriteLogFileAsync ( T.Exception, ex.AsString () );
throw new Exception ( "In FileHelper.UploadTextFileAsync " + ex.ToString (), ex.InnerException );
}
tries++;
}
return null;
}
public static async Task<bool> DownloadFileAsync ( DropboxClient dbx, string dropboxFilepath, string localFilepath )
// Copies given Dropbox file to given local file, deleting any pre-existing destination file
// Returns true if successful
// NOTE: Dropbox requires initial "/" in dropboxFilePath
{
int tries = 0;
while ( tries < 30 )
{
try
{
// If destination exists, delete it
if ( File.Exists ( localFilepath ) )
File.Delete ( localFilepath );
// Copy file
using ( var response = await dbx.Files.DownloadAsync ( dropboxFilepath ) )
{
using ( FileStream fileStream = File.Create ( localFilepath ) )
{
( await response.GetContentAsStreamAsync() ).CopyTo ( fileStream );
}
}
return true;
}
catch ( RateLimitException ex )
{
// We have to back off and retry later
int backoffSeconds= ex.RetryAfter; // >= 0
System.Diagnostics.Debug.WriteLine ( "****** Dropbox requested backoff of " + backoffSeconds.ToString () + " seconds" );
await Task.Delay ( backoffSeconds * 1000 );
}
catch ( Exception ex )
{
await Utility.WriteLogFileAsync ( T.Exception, ex.AsString () );
}
tries++;
}
return false;
}
public static async Task EnsureSubfolderExistsAsync ( DropboxClient dbx, string subfolderPath )
// Creates given subfolder for given client unless it already exists
{
if ( await ExistsAsync ( dbx, subfolderPath ) )
return;
await CreateFolderAsync ( dbx, subfolderPath);
}
}
}
using Xamarin.Forms;
using Xamarin.Essentials;
namespace MyApp
{
public class Utility
{
public static async Task SetSettingAsync ( string key, string settingValue )
// Stores given value in setting whose key is given
// Uses secure storage if possible, otherwise uses preferences
{
try
{
await SecureStorage.SetAsync ( key, settingValue );
}
catch
{
// On some Android devices, secure storage is not supported - here if that is the case
// Use preferences
Preferences.Set ( key, settingValue );
}
}
public static async Task<string> GetSettingAsync ( string key )
// Returns setting with given name, or null if unavailable
// Uses secure storage if possible, otherwise uses preferences
{
string settingValue;
try
{
settingValue = await SecureStorage.GetAsync ( key );
}
catch
{
// Secure storage is unavailable on this device so use preferences
settingValue = Preferences.Get ( key, defaultValue: null );
}
return settingValue;
}
In the Dropbox app console, permission type is Scoped App (App Folder), and permissions are files.content.write and files.content.read.

Related

RAPID API ERROR: EXPECTED BEGIN_ARRAY BUT WAS BEGIN_OBJECT AT LINE 1 COLUMN 2 PATH $ BY USING RETROFIT

I am using Covid Tracker Rapid API for a recipe app project. The problem occurs when trying to making GET requests to the Rapid API. The error shows expected begin array but was begin object at line 1 column 2 path $
This is my Retrofit API Class
public interface Api {
String BASE_URL = "https://who-covid-19-data.p.rapidapi.com/api/data/";
#GET("names")
Call < List < Model > > modelData( #Header("X-RapidAPI-Host") String api,
#Header("X-RapidAPI-Key") String apiKey );
}
This is my model class
public class Model {
String names;
public Model ( String names ) {
this.names = names;
}
public String getNames () {
return names;
}
public void setNames ( String names ) {
this.names = names;
}
}
This is my main activity class
public class MainActivity extends AppCompatActivity {
TextView textView;
#Override
protected void onCreate ( Bundle savedInstanceState ) {
super.onCreate ( savedInstanceState );
setContentView ( R.layout.activity_main );
textView=findViewById ( R.id.txtHell );
loadData();
}
private void loadData () {
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(Api.BASE_URL)
.addConverterFactory( GsonConverterFactory.create())
.addConverterFactory ( ScalarsConverterFactory.create () )
.build();
Api api = retrofit.create(Api.class);
Call < List < Model > > call=api.modelData ("","");
call.enqueue ( new Callback < List < Model > > ( ) {
#Override
public void onResponse ( Call < List < Model > > call , Response < List < Model > > response ) {
if(!response.isSuccessful ()){
textView.setText ( "Code"+response.code () );
}else{
List<Model> models=response.body ();
for(Model model:models){
String data="";
data +="Names :"+ model.getNames ();
textView.append (data );
}
}
}
#Override
public void onFailure ( Call < List < Model > > call , Throwable t ) {
textView.setText ( t.getMessage () );
}
} );
}
}
please help me
Everything is good with API and RapidAPI. I believe you just need to change the interface like this.
#GET("names")
Call <ListModel> modelData( #Header("X-RapidAPI-Host") String api,
#Header("X-RapidAPI-Key") String apiKey );

Would creating an MSSqlServerSink within a web request create a memory leak or similar type of problem?

I have a fairly unique multi-tenant application where each client gets their own database. I'm currently using Serilog with an MSSqlServerSink to log everything to a single database. I just got the request/requirement to also log to the individual tenant databases.
I created a new ILogEventSink implementation that uses a ConcurrentDictionary and look for an existing sink, then creates a new one if it doesn't exist.
Since deploying, we've been getting some 503 errors and the only symptoms being observed by our admins are a number of connections to the web server seem to be staying open in a CLOSE_WAIT state. I'm searching for more info on what causes CLOSE_WAIT, but the only big change was the deployment of this new logging sink.
Since the only reference to the new sink is maintained in my sink object (and that object is created during app_start), I would think this would work, but is it possible that this new sink is somehow getting tied to the thread that's handling the current request and keeping that request/connection alive?
using Serilog.Core;
using Serilog.Events;
using Serilog.Sinks.MSSqlServer;
using System;
using System.Collections.Concurrent;
using System.Linq;
using Context = Logging.Constants.Context;
namespace Web.Logging
{
public class CustomerSink : ILogEventSink
{
private readonly ConcurrentDictionary<string, SinkCache> Sinks;
private readonly ICustomerProvider CustomerProvider;
public CustomerSink( ICustomerProvider customerProvider = null )
{
CustomerProvider = customerProvider ?? Customer.GetProvider();
Sinks = new ConcurrentDictionary<string, SinkCache>();
}
private ILogEventSink CreateSink( Customer customer )
{
var columnOptions = SqlServerOptions.DefaultColumnOptions();
var remove = columnOptions.AdditionalColumns.Where( column => column.ColumnName == Context.ApplicationName || column.ColumnName == Context.CustomerId ).ToList();
foreach( var column in remove )
columnOptions.AdditionalColumns.Remove( column );
columnOptions.Store.Remove( StandardColumn.MessageTemplate );
columnOptions.LogEvent.ExcludeAdditionalProperties = true;
columnOptions.LogEvent.ExcludeStandardColumns = true;
return new MSSqlServerSink(
connectionString: customer.ConnectionString,
tableName: "Event",
batchPostingLimit: 50,
period: TimeSpan.FromSeconds( 5 ),
formatProvider: null,
autoCreateSqlTable: true,
columnOptions: columnOptions,
schemaName: "log"
);
}
private SinkCache CreateSink( string customerId )
{
if( CustomerProvider.GetCustomer( customerId, out var customer ) )
return new SinkCache( customer.ConnectionString, CreateSink( customer ) );
return new SinkCache( null, null );
}
public void Emit( LogEvent logEvent )
{
if( logEvent.Properties.TryGetValue( Context.CustomerId, out var value ) && value is ScalarValue scalar && scalar.Value != null )
{
var cache = Sinks.AddOrUpdate( scalar.Value.ToString(), CreateSink,
( customerId, existing ) =>
{
if( existing.Expiration < DateTime.UtcNow )
{
if( CustomerProvider.GetCustomer( customerId, out var customer ) )
{
if( customer.ConnectionString == existing.ConnectionString )
return new SinkCache( existing.ConnectionString, existing.Sink ); //Just refresh the expiration
if( existing.Sink is IDisposable disposable )
disposable.Dispose();
return new SinkCache( customer.ConnectionString, CreateSink( customer ) );
}
else if( existing.Sink is IDisposable disposable )
disposable.Dispose();
return new SinkCache( null, null );
}
//No change
return existing;
} );
cache.Sink?.Emit( logEvent );
}
}
private class SinkCache
{
public string ConnectionString { get; }
public DateTime Expiration { get; }
public ILogEventSink Sink { get; }
public SinkCache( string connectionString, ILogEventSink sink )
{
ConnectionString = connectionString;
Sink = sink;
Expiration = DateTime.UtcNow.AddMinutes( 2 );
}
}
}
}
Serilog sinks often need to be disposed in order to clean up eagerly. MSSqlServerSink is IDisposable, and while resources held by undisposed instances will eventually be cleaned up by the .NET finalizer thread, resources will be tied up for an unspecified amount of time before finalization kicks in, leading to leak-like behavior.
Your solution will need to be modified so that the sinks are disposed, or, you could use Serilog.Sinks.Map instead of this to route tenant-specific logs to the correct sink and get sink caching/disposing implemented for you.

Issue with Swagger Docs generated on api versioning support on a single controller

I have two Swagger docs generated by Swashbuckle, namely docs/v1 and docs/v2. However docs/v2 doesn't provide information on the action GetV2(). Please help if Swashbuckle has an option to address this.
1.Since the route template appears to be same for actions get() and getv2(), docs v2 doesn't show any info about getV2().
2. Swagger definition doesn't look v1.0/get while appears as v{version}/get in docs/v1
Note:I have refered on apiversioning samples, but not sure what am I missing. All samples refer to Swashbuckle.core while i use Swashbuckle.
[ApiVersion("1.0")]
[ApiVersion("2.0")]
public class HelloController : ApiControllerBase
{
[MapToApiVersion("1.0")]
[Route("v{version:apiVersion}/get")]
[HttpGet]
public ProjectSightActionResult Get()
{
return new Ok("Version 1.0");
}
[MapToApiVersion("2.0")]
[Route("v{version:apiVersion}/get")]
[HttpGet]
public ProjectSightActionResult GetV2()
{
return new Ok("Version 2.0");
}
}
This is my controller including two actions, one for version v1 and one for v2. Below is the webapi.config for the route constraint:
public static void Register(HttpConfiguration config)
{
// Web API configuration and services
// Version Start
// https://github.com/Microsoft/aspnet-api-versioning/wiki/Versioning-via-the-URL-Path
// added to the web api configuration in the application setup
var constraintResolver = new DefaultInlineConstraintResolver()
{
ConstraintMap = {["apiVersion"] = typeof( ApiVersionRouteConstraint )}
};
config.MapHttpAttributeRoutes(constraintResolver);
config.AddApiVersioning();
// Version End
// Web API routes
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
config.Filters.Add(new AuthenticationFilter());
// This causes Web API to remove the IPrincipal from any request that enters the Web API pipeline. Effectively, it "un-authenticates" the request.
// https://learn.microsoft.com/en-us/aspnet/web-api/overview/security/authentication-filters
config.SuppressHostPrincipal();
}
My Swagger config has the code:
[assembly: PreApplicationStartMethod(typeof(SwaggerConfig), "Register")]
namespace sample.WebAPI
{
public class SwaggerConfig
{
public static void Register()
{
var thisAssembly = typeof(SwaggerConfig).Assembly;
GlobalConfiguration.Configuration
.EnableSwagger(c =>
{
c.MultipleApiVersions(
(apiDesc, targetApiVersion) => ResolveVersionSupportByRouteConstraint(apiDesc, targetApiVersion),
vc =>
{
vc.Version("v1", "sample.WebAPI");
vc.Version("v2", "sample.WebAPI");
});
}
)
.EnableSwaggerUi(c =>
{
c.EnableDiscoveryUrlSelector();
// If your API supports ApiKey, you can override the default values.
// "apiKeyIn" can either be "query" or "header"
c.EnableApiKeySupport("x-jwt-assertion", "header");
});
}
private static string GetXmlCommentsPath()
{
return string.Format(#"{0}\bin\XmlComments.xml", AppDomain.CurrentDomain.BaseDirectory);
}
private static bool ResolveVersionSupportByRouteConstraint(ApiDescription apiDesc, string targetApiVersion)
{
//check for deprecated versions
var controllerVersionAttributes = apiDesc.ActionDescriptor.ControllerDescriptor.GetCustomAttributes<ApiVersionAttribute>(true);
if (!controllerVersionAttributes.Any())
{
return true; // include when no attributes are defined
}
if (targetApiVersion.StartsWith("v"))
{
targetApiVersion = targetApiVersion.Substring(1); // remove the leading "v" in `v{x.x}`
}
var apiVersion = ApiVersion.Parse(targetApiVersion);
var controllerApiVersion = controllerVersionAttributes
.Where(x => x.Versions.Contains(apiVersion))
.FirstOrDefault();
// has a compatible version, now check the action for [MapToApiVersion]
if (controllerApiVersion != null)
{
var actionMapToAttributes = apiDesc.ActionDescriptor.GetCustomAttributes<MapToApiVersionAttribute>(false);
if (!actionMapToAttributes.Any())
{
return true; // no MapTo attributes matched, then include the action
}
if (actionMapToAttributes.Any(x => x.Versions.Contains(apiVersion)))
{
return true; // include mapped action
}
}
return false;
}
}
}
I'm not sure if you ever solved your problem, but you can now use the official API Explorer for API versioning, which makes Swagger integration simple. You can see a complete working example here.
Here's an abridged version that should work for you:
static void Register( HttpConfiguration configuration )
{
var constraintResolver = new DefaultInlineConstraintResolver() { ConstraintMap = { ["apiVersion"] = typeof( ApiVersionRouteConstraint ) } };
configuration.AddApiVersioning();
configuration.MapHttpAttributeRoutes( constraintResolver );
// note: this option is only necessary when versioning by url segment.
// the SubstitutionFormat property can be used to control the format of the API version
var apiExplorer = configuration.AddVersionedApiExplorer( options => options.SubstituteApiVersionInUrl = true );
configuration.EnableSwagger(
"{apiVersion}/swagger",
swagger =>
{
// build a swagger document and endpoint for each discovered API version
swagger.MultipleApiVersions(
( apiDescription, version ) => apiDescription.GetGroupName() == version,
info =>
{
foreach ( var group in apiExplorer.ApiDescriptions )
{
var description = "A sample application with Swagger, Swashbuckle, and API versioning.";
if ( group.IsDeprecated )
{
description += " This API version has been deprecated.";
}
info.Version( group.Name, $"Sample API {group.ApiVersion}" )
.Contact( c => c.Name( "Bill Mei" ).Email( "bill.mei#somewhere.com" ) )
.Description( description )
.License( l => l.Name( "MIT" ).Url( "https://opensource.org/licenses/MIT" ) )
.TermsOfService( "Shareware" );
}
} );
swagger.IncludeXmlComments( XmlCommentsFilePath );
} )
.EnableSwaggerUi( swagger => swagger.EnableDiscoveryUrlSelector() );
}
}
static string XmlCommentsFilePath
{
get
{
var basePath = System.AppDomain.CurrentDomain.RelativeSearchPath;
var fileName = typeof( Startup ).GetTypeInfo().Assembly.GetName().Name + ".xml";
return Path.Combine( basePath, fileName );
}
}

Host and load KML File in Flex project (IGN)

I'm looking at a tutorial to display placemarks using a KML file on a flex application. I'm using IGN API (openscales) in flex project.
The example works perfectly (http://openscales.org/userguide/examples/srcview/source/KMLExample.mxml.
<os:KML url="http://www.parisavelo.net/velib.kml"
proxy="http://openscales.org/proxy.php?url="
numZoomLevels="20"
style="{Style.getDefaultCircleStyle()}"/>
But when I'm hosting the same kml file on my server like that :
<os:KML url="http://www.cycom.org/velib.kml"
proxy="http://openscales.org/proxy.php?url="
numZoomLevels="20"
style="{Style.getDefaultCircleStyle()}"/>
Placemarks don't show up on the map. I tried to host the kml file on different hosts but that doesn't change. Do you have a clue ?
Thank you.
You need to add a crossdomain.xml to the server side (in root folder) to allow the application to access your KML file. For example, if you want to allow access to your server to all IPs and domain names just use a simple wild card as below in your crossdomain.xml file:
<?xml version="1.0"?>
<cross-domain-policy>
<allow-access-from domain="*" />
</cross-domain-policy>
For more information about using crossdomain file see: Transfering Data Accross Domains Using crossdomain.xml
If you don't have access to the server or if this solution doesn't work for you, you have to modify the KML.as class in openscales (found in org.openscales.core.layer package).
Change line 19
private var _request: XMLRequest = null;
to
private var _request :URLLoader = null;
Here is the entire modified KML class:
package org.openscales.core.layer
{
import flash.events.Event;
import flash.events.IOErrorEvent;
import flash.net.URLLoader;
import flash.net.URLRequest;
import flash.net.URLRequestMethod;
import org.openscales.core.Trace;
import org.openscales.core.feature.Feature;
import org.openscales.core.format.KMLFormat;
import org.openscales.core.request.XMLRequest;
import org.openscales.geometry.basetypes.Bounds;
public class KML extends FeatureLayer
{
private var _url :String = "";
private var _request :URLLoader = null;
private var _kmlFormat :KMLFormat = null;
private var _xml :XML = null;
public function KML ( name :String,
url :String,
bounds :Bounds = null )
{
this._url = url;
this.maxExtent = bounds;
super( name );
this._kmlFormat = new KMLFormat();
}
override public function destroy () :void
{
if ( this._request )
this._request = null;
this.loading = false;
super.destroy();
}
override public function redraw ( fullRedraw :Boolean = true ) :void
{
if ( !displayed )
{
this.clear();
return;
}
if ( !this._request )
{
this.loading = true;
this._request = new URLLoader();
this._request.addEventListener( Event.COMPLETE, onSuccess );
this._request.addEventListener( IOErrorEvent.IO_ERROR, onFailure );
this._request.load( new URLRequest( url ));
}
else
{
this.clear();
this.draw();
}
}
public function onSuccess ( event :Event ) :void
{
this.loading = false;
var loader :URLLoader = event.target as URLLoader;
// To avoid errors if the server is dead
try
{
this._xml = new XML( loader.data );
if ( this.map.baseLayer.projection != null && this.projection != null && this.projection.srsCode != this.map.baseLayer.projection.srsCode )
{
this._kmlFormat.externalProj = this.projection;
this._kmlFormat.internalProj = this.map.baseLayer.projection;
}
this._kmlFormat.proxy = this.proxy;
var features :Vector.<Feature> = this._kmlFormat.read( this._xml ) as Vector.<Feature>;
this.addFeatures( features );
this.clear();
this.draw();
}
catch ( error :Error )
{
Trace.error( error.message );
}
}
protected function onFailure ( event :Event ) :void
{
this.loading = false;
Trace.error( "Error when loading kml " + this._url );
}
public function get url () :String
{
return this._url;
}
public function set url ( value :String ) :void
{
this._url = value;
}
override public function getURL ( bounds :Bounds ) :String
{
return this._url;
}
}
}

URLStream throws Error#2029 in my flex AIR app

In my AIR app, I am trying to implement a file downloader using URLStream.
public class FileDownloader {
// Class to download files from the internet
// Function called every time data arrives
// called with an argument of how much has been downloaded
public var onProgress :Function = function(loaded:Number, total:Number):void{};
public var onComplete :Function = function():void{};
public var remotePath :String = "";
public var localFile :File = null;
public var running:Boolean = false;
public var stream :URLStream;
private var fileAccess :FileStream;
public function FileDownloader( remotePath :String = "" , localFile :File = null ) {
this.remotePath = remotePath;
this.localFile = localFile;
}
public function load() :void
{
try
{
stream = null;
if( !stream || !stream.connected )
{
stream = new URLStream();
fileAccess = new FileStream();
var requester :URLRequest = new URLRequest( remotePath );
var currentPosition :uint = 0;
var downloadCompleteFlag :Boolean = false;
// Function to call oncomplete, once the download finishes and
// all data has been written to disc
fileAccess.addEventListener( "outputProgress", function ( result ):void {
if( result.bytesPending == 0 && downloadCompleteFlag ) {
stream.close();
fileAccess.close();
running = false;
onComplete();
}
});
fileAccess.openAsync( localFile, FileMode.WRITE );
fileAccess.addEventListener(IOErrorEvent.IO_ERROR, function(e:IOErrorEvent)
{
trace('remotePath: '+remotePath);
trace('io error while wrintg ....'+e.toString());
});
stream.addEventListener(IOErrorEvent.IO_ERROR, function(e:IOErrorEvent)
{
trace('remotePath: '+remotePath);
trace('There was an IO error with the stream: '+e.text);
});
stream.addEventListener( "progress" , function (e:ProgressEvent) :void {
var bytes :ByteArray = new ByteArray();
var thisStart :uint = currentPosition;
currentPosition += stream.bytesAvailable;
// ^^ Makes sure that asyncronicity does not break anything
try
{
//trace('reading from '+remotePath+' ...');
stream.readBytes( bytes, thisStart );
fileAccess.writeBytes( bytes, thisStart );
}
catch(err:Error)
{
trace('remotePath: '+remotePath);
trace('error while writing bytes from...'+err.name+':'+err.message);
if(stream.connected)
stream.close();
abort();
onComplete();
return;
}
onProgress( e.bytesLoaded, e.bytesTotal );
});
stream.addEventListener( "complete", function () :void {
downloadCompleteFlag = true;
});
stream.load( requester );
} else {
// Do something unspeakable
}
running = true;
}
catch(err:Error)
{
trace('error while downloading the file: '+err);
}
}
public function abort():void {
try {
stream.close();
trace('stream closed');
running = false;
}
catch(err:Error) {
trace('error while aborting download');
trace(err);
}
}
}
I simply create an object of the above class and passing the url and the file and call the load function. For some files I get the following error.
remotePath: http://mydomain.com/238/6m_608-450.jpg
error while writing bytes from...Error:Error #2029: This URLStream object does not have a stream opened.
Which means the error is from the file stream(fileAccess) that I am using. I am unable to figure out why this could be happening. If I try to open the url http://mydomain.com/238/6m_608-450.jpg in the browser, it opens properly. This happens randomly for some files. What could be the problem?
I have tried in my office and it works for me (for differents files and filesize).
So, can you describe the files (or types files) which don't work for you (post an url if you can) ?
I would say that when you use the method readBytes your stream (so the URLStream) is ever close.
More, I allows me some advice :
1/ Use flash's constants instead of simple string
2/ Don't forget to remove your listeners once the operation completed
3/ Your method FileDownloader is quite confusing. Use lowercase if it's a function or puts a capital letter with class's name if you use it as a constructor. For me, this function must be a constructor.

Resources