I created a bunch of small and mostly internal WPF applications for my company - usually for some file manipulation etc. For these programs, I would like to start collecting user info - who uses them, how often, most importantly - which version is used.
It's partly for my personal skill development, but I also want to introduce auto-updating for my apps - some of them have this feature already, but they rely on text files located on shared drive so obviously not a good solution.
The intended audience is not a large crowd, probably fewer than 50 users. The calls to the database will most likely happen only on the application launch.
The idea is that I have a generic 'helper' dll, which I reference in my projects so that in each of the programs I call a simple method and have the repetitive registration handling out of my mind. All the data is stored in a single Azure database called MyDB, and all the programs should connect through the same API (also hosted in Azure), called e.g. MyApi (myapi.azuresites.net).
So, looking from the point of view of a single client app:
public MainWindow()
{
InitializeComponent();
Register = new RegistrationHelper();
var progressInfo = new Progress<string>(p => StatusInfo = p);
Register.HandleRegistration(progressInfo);
//and that's it
}
Now, the helper library contains a smart constructor that will deduct all the info I need for my database (email is optional only for some programs with explicit registration):
private const string DefaultApiAddress = "https://MyApi.azuresites.net";
public RegistrationHelper(string email = null)
{
ApiUri = new Uri(DefaultApiAddress);
UserName = Environment.UserName;
MachineName = Environment.MachineName;
//email is not needed for registration
UserProvidedEmail = email;
CurrentAssembly = Assembly.GetEntryAssembly();
ProgramName = CurrentAssembly.GetName().Name;
ProgramVersion = FileVersionInfo.GetVersionInfo(CurrentAssembly.Location).FileVersion;
}
Then it has a public method for running handling the registration (which obviously should not block program execution):
public void HandleRegistration(IProgress<string> progressInfo)
{
Task.Run((() => HandleRegistrationAsync(progressInfo)));
}
Which calls a private handler:
private async Task HandleRegistrationAsync(IProgress<string> progressInfo)
{
try
{
progressInfo.Report("Connecting...");
RegisteredUserId = await GetUserIdAsync();
if (RegisteredUserId == 0)
{
RegisteredUserId = await RegisterUserByNameAsync();
if (RegisteredUserId > 0)
{
progressInfo.Report(string.Format("Newly registered. Id: {0}", RegisteredUserId));
}
else
{
progressInfo.Report("Registration failed");
}
}
else if (RegisteredUserId < 0)
{
progressInfo.Report(string.Format("Not registered. {0}", RegisteredUserId));
}
else
{
progressInfo.Report(string.Format("Updating user {0} info...", RegisteredUserId));
progressInfo.Report("Registered. Id: " + RegisteredUserId);
}
}
catch (Exception ex)
{
progressInfo.Report("Connection error");
if (ThrowExceptions)
throw ex;
}
And the methods that call the API are all pretty similar, structured as follows:
public async Task<int> UpdateProgramUsageAsync()
{
try
{
using (var client = new HttpClient())
{
client.BaseAddress = ApiUri;
client.DefaultRequestHeaders.Accept.Clear();
HttpResponseMessage response = await client.GetAsync(string.Format("api/update/{0}/{1}/{2}/", RegisteredUserId, ProgramName, ProgramVersion));
return response.IsSuccessStatusCode
? Convert.ToInt32(response.Content.ReadAsStringAsync().Result)
: -1;
}
}
catch (Exception)
{
if (ThrowExceptions)
throw;
else
return -666;
}
}
Now, what's in the web API:
There is a single UserController
(apart from the stuff that comes out of the box from the WebApi template). It contains the connection string to the database (hardcoded), plus a bunch of constants for StoredProcedures
names and stuff like that.
Then, my controller methods are structured as follows:
private const string ConnectionString = "allthestuffsensitivetoconnecttodatabase";
[Route("api/getid/{username}")]
public async Task<int> GetIdAsync(string username)
{
using (var conn = new SqlConnection(ConnectionString))
{
conn.Open();
if (conn.State == ConnectionState.Open)
{
var result = await ExecuteScalarCommandAsync(GetRegisteredUserByName, conn,
new[]
{
new SqlParameter(UserNameCol, username),
});
if (result != null && !(result is DBNull))
{
return Convert.ToInt32(result);
}
return 0;
}
}
return -1;
}
private async Task<object> ExecuteScalarCommandAsync(string commandName, SqlConnection conn, SqlParameter[] parameters)
{
using (var command = new SqlCommand(commandName, conn)
{
CommandType = CommandType.StoredProcedure,
})
{
command.Parameters.AddRange(parameters);
return await command.ExecuteScalarAsync();
}
}
Also, as for the methods that not only 'get' values, but also update database, I am using the GET as well.
The one below is a match of UpdateProgramUsageAsync()
pasted above:
[Route("api/update/{userid}/{programname}/{version}")]
[AcceptVerbs("GET")]
public async Task<int> UpdateProgramUsageAsync(string userid, string programname, string version)
{
using (var conn = new SqlConnection(ConnectionString))
{
conn.Open();
if (conn.State == ConnectionState.Open)
{
var result = await ExecuteScalarCommandAsync(UpdateProgramUsage, conn,
new[]
{
new SqlParameter(UserIdCol, userid),
new SqlParameter(ProgramNameCol, programname),
new SqlParameter(VersionCol, version)
}
);
if (result != null && !(result is DBNull))
{
return Convert.ToInt32(result);
}
}
}
return 0;
}
As you can see, it relies on stored procedures updating the database and returning some response codes. The procedures are pretty simple (get value, put value into a table, nothing fancy).