With SerilogWeb.Owin discontinued, is there an "official" integration? - asp.net-web-api

I came across the discontinuation notice of the SerilogWeb.Owin package, and in reading the GitHub issue there was discussion about "redirecting folks somewhere" given the ~5K+ downloads of the package.
But I haven't been able to figure out where I'm being redirected to!
So where should I be looking for a "Serilog-blessed" integration for using Serilog with an (OWIN) self-hosted Web API?

The package boils down to the following, which I derived from the issues in [the repo](https://github.com/serilog-web/owin/blob/master/src/SerilogWeb.Owin/Owin/LoggerFactory.cs
):
Adding a RequestId to allow traces to be correlated
using (Serilog.Context.LogContext.PushProperty("RequestId", Guid.NewGuid().ToString("N"))
Plug in a logger redirector:
app.SetLoggerFactory(new SerilogOwinFactory());
Copy in the impl (this works with WebApi 5.2.4, Katana 4.0, Serilog 2.6) and incorporates learnings from another question about efficiently forwarding events without the writing treating the message as a template, and ensuring the Exception is included a first class element of the message in order that it can be formatted and/or demystyfied corrrectly:
And the good news is that, when you reach ASP.NET Core, there will be a fully maintained package with a deeper integration waiting for you, with a one-liner hookin ;)
// From https://github.com/serilog-web/owin/blob/master/src/SerilogWeb.Owin/Owin/LoggerFactory.cs
// Copyright 2015 SerilogWeb, Serilog Contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
using System;
using System.Diagnostics;
using System.Linq;
using Microsoft.Owin.Logging;
using Serilog;
using Serilog.Core;
using Serilog.Events;
using ILogger = Serilog.ILogger;
namespace SerilogWeb.Owin
{
/// <summary>
/// Implementation of Microsoft.Owin.Logger.ILoggerFactory.
/// </summary>
public class SerilogOwinFactory : ILoggerFactory
{
readonly Func<ILogger> _getLogger;
readonly Func<TraceEventType, LogEventLevel> _getLogEventLevel;
/// <summary>
/// Create a logger factory.
/// </summary>
/// <param name="logger">The logger; if not provided the global <see cref="Serilog.Log.Logger"/> will be used.</param>
/// <param name="getLogEventLevel"></param>
public SerilogOwinFactory(ILogger logger = null, Func<TraceEventType, LogEventLevel> getLogEventLevel = null)
{
_getLogger = logger == null ? (Func<ILogger>)(() => Log.Logger) : (() => logger);
_getLogEventLevel = getLogEventLevel ?? ToLogEventLevel;
}
/// <summary>
/// Creates a new ILogger instance of the given name.
/// </summary>
/// <param name="name">The logger context name.</param>
/// <returns>A logger instance.</returns>
public Microsoft.Owin.Logging.ILogger Create(string name)
{
return new Logger(_getLogger().ForContext(Constants.SourceContextPropertyName, name), _getLogEventLevel);
}
static LogEventLevel ToLogEventLevel(TraceEventType traceEventType)
{
switch (traceEventType)
{
case TraceEventType.Critical:
return LogEventLevel.Fatal;
case TraceEventType.Error:
return LogEventLevel.Error;
case TraceEventType.Warning:
return LogEventLevel.Warning;
case TraceEventType.Information:
return LogEventLevel.Information;
case TraceEventType.Verbose:
return LogEventLevel.Verbose;
case TraceEventType.Start:
return LogEventLevel.Debug;
case TraceEventType.Stop:
return LogEventLevel.Debug;
case TraceEventType.Suspend:
return LogEventLevel.Debug;
case TraceEventType.Resume:
return LogEventLevel.Debug;
case TraceEventType.Transfer:
return LogEventLevel.Debug;
default:
throw new ArgumentOutOfRangeException("traceEventType");
}
}
class Logger : Microsoft.Owin.Logging.ILogger
{
readonly ILogger _logger;
readonly Func<TraceEventType, LogEventLevel> _getLogEventLevel;
static readonly Exception _exceptionPlaceHolder = new Exception("(Exception enclosed)");
internal Logger(ILogger logger, Func<TraceEventType, LogEventLevel> getLogEventLevel)
{
_logger = logger;
_getLogEventLevel = getLogEventLevel;
}
public bool WriteCore(TraceEventType eventType, int eventId, object state, Exception exception, Func<object, Exception, string> formatter)
{
var level = _getLogEventLevel(eventType);
// According to docs http://katanaproject.codeplex.com/SourceControl/latest#src/Microsoft.Owin/Logging/ILogger.cs
// "To check IsEnabled call WriteCore with only TraceEventType and check the return value, no event will be written."
if (state == null)
return _logger.IsEnabled(level);
if (!_logger.IsEnabled(level))
return false;
var formattedMessage = formatter(state, null); // Omit exception as we're including it in the LogEvent
var template = new Serilog.Events.MessageTemplate(new[] { new Serilog.Parsing.TextToken(formattedMessage) });
var logEvent = new Serilog.Events.LogEvent(DateTimeOffset.Now, level, exception, template, Enumerable.Empty<Serilog.Events.LogEventProperty>());
_logger.ForContext("eventId", eventId).Write(logEvent);
return true;
}
}
}
}

Related

Windows 10 version 1903 update (NET Framework 4.8) breaking Prism 6.3

First off, we are well aware that this isn't strictly speaking a Prism 6.3 issue; what we're looking for (in case a solution isn't straightforward) are pointers to a solution to the problem -thanks in advance btw-, which is:
Windows 10 version 1903, via .NET Framework 4.8 inclusion, breaks our otherwise perfectly functioning, tried-and-true, production-deployed Prism 6.3-based commercial software. We're using Prism (Core), Prism.WPF, and Prism.MEF (all v6.3). What we get (source code later) is the following runtime error whenever we try to instantiate a registered View:
Prism.Regions.RegionNavigationService.CreateNewRegionItem(String candidateTargetContract) throws an InvalidOperationException: Cannot create navigation target 'xyzView'. Activation error ocurred while tring to get instance of type Object, key 'xyzView'.
Inner exception stems from Microsoft.Practices.ServiceLocation.ServiceLocatorImplBase.GetInstance(Type serviceType, String key), which throws an ActivationException: Activation error ocurred while tring to get instance of type Object, key 'xyzView'.
Devil's in the details so here's some relevant code (xyzView = ExpedicionView or ExpedicionMaestroView, both trigger the Exception):
[ModuleExport(typeof(ExpedicionModulo))]
[Export(typeof(IMenu))]
public class ExpedicionModulo : IModule, IMenu
{
[Import]
public IRegionManager RegionManager;
[Import]
public ILoggerFacade Logger;
[ImportingConstructor]
public ExpedicionModulo(IRegionManager regionManager, ILoggerFacade logger)
{
Logger = logger;
RegionManager = regionManager;
// irrelevant (for our purposes) code omitted here
}
public void Initialize()
{
// Here's how we register views for main region
RegionManager.RegisterViewWithRegion(RegionNames.MainContentRegion, typeof(ExpedicionView));
RegionManager.RegisterViewWithRegion(RegionNames.MainContentRegion, typeof(MaestroExpedicionView));
// Some more registering for our dialog region
RegionManager.RegisterViewWithRegion(RegionNames.DialogRegion, typeof(ExpedicionDetalleView));
// other views registered in the very same fashion
Logger.Log("Expedition Module initialized", Category.Info, Priority.None);
}
We invoke RequestNavigate in this bit of code here:
[Export]
[PartCreationPolicy(CreationPolicy.Shared)]
public class ShellViewModel : BaseViewModel
{
public IRibbonPrincipal RibbonPrincipal { get; set; }
private readonly InteractionRequest<Confirmation> _confirmationInteractionRequest;
private readonly InteractionRequest<Notification> _notificationInteractionRequest;
private string _seccionActiva;
private string _subseccionActiva;
private IRegionManager _regionManager;
[ImportingConstructor]
public ShellViewModel(IRegionManager regionManager, IEventAggregator eventAggregator, ILoggerFacade logger)
: this(eventAggregator, logger)
{
try
{
_regionManager = regionManager;
_regionManager.RegisterViewWithRegion(RegionNames.MainContentRegion, typeof(PrincipalView));
}
catch (Exception ex)
{
this.LoggerError(ex.Message);
}
}
public ShellViewModel(IEventAggregator eventAggregator, ILoggerFacade logger)
: base(eventAggregator, logger)
{
try
{
// Some irrelevant (to our purposes) initialization code omitted here
New = new DelegateCommand(() => { }, () => { return false; }); // etc.
_confirmationInteractionRequest = new InteractionRequest<Confirmation>();
_notificationInteractionRequest = new InteractionRequest<Notification>();
// Events code omitted for brevity sake
EventAggregator.GetEvent<MessageBoxEvent>().Subscribe(ShowMessageBox, ThreadOption.PublisherThread, false); // etc.
// View loading wireup
CambioSeccion = new DelegateCommand<object>(OnCambioSeccion);
CambioSubseccion = new DelegateCommand<object[]>(OnCambioSubseccion);
// some more irrelevant (to our purposes) code omitted here.
}
catch (Exception ex)
{
this.LoggerError(ex.Message);
}
}
/// <summary>
/// This is where we instantiate the View (WAI in .NET Framework <=4.7.2)
/// </summary>
/// <param name="objeto"></param>
private void OnCambioSeccion(object objeto)
{
// Ribbon menu handling here
RibbonPrincipal.ResetRibbon();
IIdentifyViewModel ivm = null;
// We are passing the View's name via clicked TreeViewItem in this case (param objeto)
TreeViewItem treeViewItem = (TreeViewItem)objeto;
if (treeViewItem != null && !string.IsNullOrEmpty(treeViewItem.Name))
{
_seccionActiva = treeViewItem.Name;
// Now we build the actual RequestNavigate invoke
// In our case, _seccionActiva would equal "Expedicion" or "MaestroExpedicion"
_regionManager.RequestNavigate(RegionNames.MainContentRegion, new Uri("/" + _seccionActiva + "View", UriKind.Relative), (r) =>
{
if (!r.Result.HasValue || !r.Result.Value)
{
// error handling code here
}
else
{
ivm = ((FrameworkElement)_regionManager.Regions[RegionNames.MainContentRegion].ActiveViews.First()).DataContext as IIdentifyViewModel;
}
});
// Some event triggering here
if (ivm != null)
{
Seccion.Cambio(EventAggregator, ivm.ID);
}
}
}
Sorry for the long winded post, and thanks in advance.
#mcandal thanks for the write-up. This looks like a recent issue introduced in .NET Framework 4.8 that we have seen with the patterns and practices activation code. The underlying issue is that in 4.8 the constructors for types were returned in a different order resulting in the patterns and practices choice of the first (eg. ctors[0]) sometimes no longer being correct.
There are a few workarounds as we work through a fix in an upcoming release.
This issue only impacts assemlbies that are ngen'd.
Workarounds:
1) Disable ngen for the assembly containing the type:
<configuration>
<runtime>
<disableNativeImageLoad>
<assemblyIdentity name="assembly_name" />
</disableNativeImageLoad>
</runtime>
</configuration>
2) For the type that is being activated only have 1 ctor (the documentation for the patterns and practices assumes there is only 1 ctor)
3) Choose the other provided activation models where you pass in the types of the parameters, to avoid the ambiguity.
As I mentioned, we are working on a fix and if you would like to discuss further you can submit a VS Feedback item and link to this post and we can continue the discussion there.

Windows 10 - programming Cortana

I am trying to programm a litte application for Cortana.
My idea is, that i say: (I enabled the "Hey Cortana" feature)
Hey Cortana, Convert 45 degrees to farenheit
and I get (in the moment) a log in my Output window (Visual Studio). I tried to say exact this sentence and Cortana understood me perfectly, but Cortana opened the browser and entered it into Bing.
Why? What did I do wrong? I don't get any Syntax Error.
This is my Code:
// commands.xml
<?xml version="1.0" encoding="utf-8"?>
<VoiceCommands xmlns="http://schemas.microsoft.com/voicecommands/1.1">
<CommandSet xml:lang="en-us" Name="MyCommands_en-us">
<CommandPrefix> Convert, </CommandPrefix>
<Example> Convert 45 degrees to farenheit </Example>
<Command Name ="farenheitToDegrees">
<Example> 73 farenheit to degrees</Example>
<ListenFor> {farenheit} farenheit to degrees </ListenFor>
<Feedback> {farenheit} are ... in degrees </Feedback>
<Navigate/>
</Command>
<Command Name="degreesToFarenheit">
<Example> 45 degrees to farenheit </Example>
<ListenFor> {degrees} degrees to farenheit </ListenFor>
<Feedback> {degrees} degrees are ... in fareneheit </Feedback>
<Navigate/>
</Command>
<PhraseTopic Label="degrees" Scenario="Dictation">
<Subject>Temperature</Subject>
</PhraseTopic>
<PhraseTopic Label="farenheit" Scenario="Dictation">
<Subject>Temperature</Subject>
</PhraseTopic>
</CommandSet>
</VoiceCommands>
// App.xaml.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.ApplicationModel;
using Windows.ApplicationModel.Activation;
using Windows.Foundation;
using Windows.Foundation.Collections;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Navigation;
using Windows.ApplicationModel.VoiceCommands;
using Windows.Storage;
using Windows.Media.SpeechRecognition;
namespace HelloWorld
{
/// <summary>
/// Provides application-specific behavior to supplement the default Application class.
/// </summary>
sealed partial class App : Application
{
/// <summary>
/// Initializes the singleton application object. This is the first line of authored code
/// executed, and as such is the logical equivalent of main() or WinMain().
/// </summary>
public App()
{
Microsoft.ApplicationInsights.WindowsAppInitializer.InitializeAsync(
Microsoft.ApplicationInsights.WindowsCollectors.Metadata |
Microsoft.ApplicationInsights.WindowsCollectors.Session);
this.InitializeComponent();
this.Suspending += OnSuspending;
}
/// <summary>
/// Invoked when the application is launched normally by the end user. Other entry points
/// will be used such as when the application is launched to open a specific file.
/// </summary>
/// <param name="e">Details about the launch request and process.</param>
protected async override void OnLaunched(LaunchActivatedEventArgs e)
{
#if DEBUG
if (System.Diagnostics.Debugger.IsAttached)
{
this.DebugSettings.EnableFrameRateCounter = true;
}
#endif
Frame rootFrame = Window.Current.Content as Frame;
// Do not repeat app initialization when the Window already has content,
// just ensure that the window is active
if (rootFrame == null)
{
// Create a Frame to act as the navigation context and navigate to the first page
rootFrame = new Frame();
rootFrame.NavigationFailed += OnNavigationFailed;
if (e.PreviousExecutionState == ApplicationExecutionState.Terminated)
{
//TODO: Load state from previously suspended application
}
// Place the frame in the current Window
Window.Current.Content = rootFrame;
}
if (rootFrame.Content == null)
{
// When the navigation stack isn't restored navigate to the first page,
// configuring the new page by passing required information as a navigation
// parameter
rootFrame.Navigate(typeof(MainPage), e.Arguments);
}
// Ensure the current window is active
Window.Current.Activate();
var storageFile =
await Windows.Storage.StorageFile.GetFileFromApplicationUriAsync(new Uri("ms-appx:///commands.xml"));
await
Windows.ApplicationModel.VoiceCommands.VoiceCommandDefinitionManager.InstallCommandDefinitionsFromStorageFileAsync(storageFile);
}
protected override void OnActivated(IActivatedEventArgs e)
{
// Was the app activated by a voice command?
if (e.Kind != Windows.ApplicationModel.Activation.ActivationKind.VoiceCommand)
{
return;
}
var commandArgs = e as Windows.ApplicationModel.Activation.VoiceCommandActivatedEventArgs;
SpeechRecognitionResult speechRecognitionResult = commandArgs.Result;
// Get the name of the voice command and the text spoken
string voiceCommandName = speechRecognitionResult.RulePath[0];
string textSpoken = speechRecognitionResult.Text;
switch (voiceCommandName)
{
case "farenheitToDegrees":
string farenheit = speechRecognitionResult.SemanticInterpretation.Properties["farenheit"][0];
System.Diagnostics.Debug.WriteLine((Convert.ToInt32(farenheit) - 32) / 1.8);
break;
case "degreesToFarenheit":
string degrees = speechRecognitionResult.SemanticInterpretation.Properties["degrees"][0];
System.Diagnostics.Debug.WriteLine(Convert.ToInt32(degrees) * 1.8 + 32);
break;
default:
System.Diagnostics.Debug.WriteLine("None of my bussiness");
break;
}
}
/// <summary>
/// Invoked when Navigation to a certain page fails
/// </summary>
/// <param name="sender">The Frame which failed navigation</param>
/// <param name="e">Details about the navigation failure</param>
void OnNavigationFailed(object sender, NavigationFailedEventArgs e)
{
throw new Exception("Failed to load Page " + e.SourcePageType.FullName);
}
/// <summary>
/// Invoked when application execution is being suspended. Application state is saved
/// without knowing whether the application will be terminated or resumed with the contents
/// of memory still intact.
/// </summary>
/// <param name="sender">The source of the suspend request.</param>
/// <param name="e">Details about the suspend request.</param>
private void OnSuspending(object sender, SuspendingEventArgs e)
{
var deferral = e.SuspendingOperation.GetDeferral();
//TODO: Save application state and stop any background activity
deferral.Complete();
}
}
}
can somebody help me?
The VCD definition file you've listed above doesn't have either a PhraseTopic or PhraseList to define the parts you've got in curly braces:
<ListenFor> {farenheit} farenheit to degrees </ListenFor>
I'm guessing you probably wanted a PhraseTopic because that allows for an unconstrained dictation suitable for a wide range of numbers, something like this:
<PhraseTopic Label="farenheit" Scenario="Dictation">
<Subject>Temperature</Subject>
</PhraseTopic>
See the spec for VCD's here on msdn, you might want to play with tweaking the Scenario value. This does mean you'll need to handle the text you get as the farenheit term yourself, of course, but typically dictated text for numbers comes through in textual '1234' form (but not in 100% of cases).
check the properties of your VCD file, values shoud be: Buil Action = Content, Copy to Output Directory = Copy always. Anyway, I hope that you registered the vcd file:
VoiceCommandService.InstallCommandSetsFromFileAsync(new Uri("ms-appx:///VCD.xml"));
Check this MVA video about Cortana: https://www.microsoftvirtualacademy.com/en-US/training-courses/universal-windows-app-development-with-cortana-and-the-speech-sdk-8487
Well... Seems that you have understood all the steps but still something is missing...
Here's an example that I have made regarding Cortana's foreground functionality:
Here's the VCD...
<?xml version="1.0" encoding="utf-8" ?>
<VoiceCommands xmlns="http://schemas.microsoft.com/voicecommands/1.2">
<CommandSet xml:lang="en-us" Name="HomeControlCommandSet_en-us">
<CommandPrefix>HomeControl</CommandPrefix>
<Example>Control alarm, temperature, light and others</Example>
<Command Name="Activate_Alarm">
<Example>Activate alarm</Example>
<ListenFor>[Would] [you] [please] activate [the] alarm [please]</ListenFor>
<ListenFor RequireAppName="BeforeOrAfterPhrase">Activate alarm</ListenFor>
<ListenFor RequireAppName="ExplicitlySpecified">Activate {builtin:AppName} alarm</ListenFor>
<Feedback>Activating alarm</Feedback>
<Navigate />
</Command>
After create this definitions, you need to registry it at App Startup:
protected async override void OnLaunched(LaunchActivatedEventArgs e)
{
...
// Install the VCD
try
{
StorageFile vcdStorageFile = await Package.Current.InstalledLocation.GetFileAsync(#"HomeControlCommands.xml");
await VoiceCommandDefinitionManager.InstallCommandDefinitionsFromStorageFileAsync(vcdStorageFile);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine("There was an error registering the Voice Command Definitions", ex);
}
}
An then override App.OnActivated method to handle when the events are triggered:
protected override void OnActivated(IActivatedEventArgs e)
{
// Handle when app is launched by Cortana
if (e.Kind == ActivationKind.VoiceCommand)
{
VoiceCommandActivatedEventArgs commandArgs = e as VoiceCommandActivatedEventArgs;
SpeechRecognitionResult speechRecognitionResult = commandArgs.Result;
string voiceCommandName = speechRecognitionResult.RulePath[0];
string textSpoken = speechRecognitionResult.Text;
IReadOnlyList<string> recognizedVoiceCommandPhrases;
System.Diagnostics.Debug.WriteLine("voiceCommandName: " + voiceCommandName);
System.Diagnostics.Debug.WriteLine("textSpoken: " + textSpoken);
switch (voiceCommandName)
{
case "Activate_Alarm":
System.Diagnostics.Debug.WriteLine("Activate_Alarm command");
break;
To see the complete tutorial, please visit this link and a working project is here. Also, if you interested in respond to the user through Cortana window, check this post regarding Cortana in background
I think you are missing a part of the command.
You're asking for Cortana to convert degrees to Fahrenheit, and the program is confused because you're not being specific enough.
If you want Cortana to covert from degrees Celsius to degrees Fahrenheit, you have to tell it specifically Celsius to Fahrenheit.

Uninitialised JsonSerializer in Breeze SaveBundleToSaveMap sample

I'm attempting to use the SaveBundleToSaveMap snippet linked below to implement custom save handling on the server side of a breeze web api implementation.
SaveBundleToSaveMap
This sample does not work as is? (see below); their is a null reference exception which could use some attention.
The SaveWorkState(provider, entitiesArray) constructor calls the ContextProvider.CreateEntityInfoFromJson(...) method which then calls (the class scoped) JsonSerializer.Deserialize(new JTokenReader(jo), entityType) method.
The issue is that JsonSerializer is uninitialised and we get a null reference exeption.
For e.g. I added this test hack to get the code running:
protected internal EntityInfo CreateEntityInfoFromJson(dynamic jo, Type entityType) {
//temp fix to init JsonSerializer if SaveChanges has NOT been called
if(JsonSerializer==null) JsonSerializer = CreateJsonSerializer();
var entityInfo = CreateEntityInfo();
entityInfo.Entity = JsonSerializer.Deserialize(new JTokenReader(jo), entityType);
entityInfo.EntityState = (EntityState)Enum.Parse(typeof(EntityState), (String)jo.entityAspect.entityState);
entityInfo.ContextProvider = this;
This issue does not occur in the standard release bits as CreateEntityInfoFromJson is always? called downstream from a SaveChanges() call which means the JsonSerializer gets initialised.
However, things would be better structured if an initialised JsonSerializer was passed to CreateEntityInfoFromJson as a parameter to avoid potential future null reference issues?
Alternately, is there a way to get the SaveBundleToSaveMap snippet to init the JsonSerializer? Its got a private setter :(
UPDATE
Implemented a very hacky stopgap solution. If anyone at IdeaBlade is watching, would be great to have a public API to convert to and from json saveBundle <-> saveMap.
/// <summary>
/// Convert a json saveBundle into a breeze SaveMap
/// </summary>`enter code here`
public static Dictionary<Type, List<EntityInfo>> SaveBundleToSaveMap(JObject saveBundle)
{
var _dynSaveBundle = (dynamic)saveBundle;
var _entitiesArray = (JArray)_dynSaveBundle.entities;
var _provider = new BreezeAdapter();
//Hack 1: Breeze.ContextProvider initializes a global JsonSerializer in its SaveChanges() method
//We are bypassing SaveChanges() and bootstrapping directly into SaveWorkState logic to generate our saveMap
//as such we need to init a serializer here and slipsteam it in via reflection (its got a private setter)
var _serializerSettings = BreezeConfig.Instance.GetJsonSerializerSettings();
var _bootstrappedJsonSerializer = JsonSerializer.Create(_serializerSettings);
//Hack 2:
//How to write to a private setter via reflection
//http://stackoverflow.com/questions/3529270/how-can-a-private-member-accessable-in-derived-class-in-c
PropertyInfo _jsonSerializerProperty = _provider.GetType().GetProperty("JsonSerializer", BindingFlags.Instance | BindingFlags.NonPublic);
//Hack 3: JsonSerializer property is on Breeze.ContextProvider type; not our derived EFContextProvider type so...
_jsonSerializerProperty = _jsonSerializerProperty.DeclaringType.GetProperty("JsonSerializer", BindingFlags.Instance | BindingFlags.NonPublic);
//Finally, we can init the JsonSerializer
_jsonSerializerProperty.SetValue(_provider, _bootstrappedJsonSerializer);
//saveWorkState constructor loads json entitiesArray into saveWorkState.EntityInfoGroups struct
var _saveWorkState = new SaveWorkState(_provider, _entitiesArray);
//BeforeSave logic loads saveWorkState.EntityInfoGroups metadata into saveWorkState.SaveMap
_saveWorkState.BeforeSave();
var _saveMap = _saveWorkState.SaveMap;
return _saveMap;
}
I looked into this. You don't actually need to make a change to the Breeze code to accomplish what you want. The ContextProvider is designed such that you can do just about whatever you want during save.
I'm curious: what "custom save handling" do you want to perform that you can't do today with the BeforeSave and AfterSave logic? I see in your "stopgap" code that you're calling BeforeSave on the SaveWorkState. What more do you need?
As an exercise, I wrote a NorthwindIBDoNotSaveContext that does what you want. Here's how it goes:
/// <summary>
/// A context whose SaveChanges method does not save
/// but it will prepare its <see cref="SaveWorkState"/> (with SaveMap)
/// so developers can do what they please with the same information.
/// See the <see cref="GetSaveMapFromSaveBundle"/> method;
/// </summary>
public class NorthwindIBDoNotSaveContext : EFContextProvider<NorthwindIBContext_CF>
{
/// <summary>
/// Open whatever is the "connection" to the "database" where you store entity data.
/// This implementation does nothing.
/// </summary>
protected override void OpenDbConnection(){}
/// <summary>
/// Perform your custom save to wherever you store entity data.
/// This implementation does nothing.
/// </summary>
protected override void SaveChangesCore(SaveWorkState saveWorkState) {}
/// <summary>
/// Return the SaveMap that Breeze prepares
/// while performing <see cref="ContextProvider.SaveChanges"/>.
/// </summary>
/// <remarks>
/// Calls SaveChanges which internally creates a <see cref="SaveWorkState"/>
/// from the <see param="saveBundle"/> and then runs the BeforeSave and AfterSave logic (if any).
/// <para>
/// While this works, it is hacky if all you want is the SaveMap.
/// The real purpose of this context is to demonstrate how to
/// pare down a ContextProvider, benefit from the breeze save pre/post processing,
/// and then do your own save inside the <see cref="SaveChangesCore"/>.
/// </para>
/// </remarks>
/// <returns>
/// Returns the <see cref="SaveWorkState.SaveMap"/>.
/// </returns>
public Dictionary<Type, List<EntityInfo>> GetSaveMapFromSaveBundle(JObject saveBundle)
{
SaveChanges(saveBundle); // creates the SaveWorkState and SaveMap as a side-effect
return SaveWorkState.SaveMap;
}
}
And here's how you could use it to get the SaveMap:
var saveMap = new NorthwindIBDoNotSaveContext().GetSaveMapFromSaveBundle(saveBundle);
Yes, it is "hacky", particularly if all you want is the SaveMap. But why do you just want the SaveMap?
We've designed the ContextProvider (and all of its sub-classes) such that you have free reign over the SaveChangesCore method. You could override that, further manipulate the SaveMap, then either delegate to the base implementation or do whatever else you have in mind for saving the entity data.
But while I don't see what you're after, it was not all that hard to extract the SaveChanges initialization logic into its own method.
So in the next release (after 1.5.2), you should find the following new method in the ContextProvider:
protected void InitializeSaveState(JObject saveBundle)
{
JsonSerializer = CreateJsonSerializer();
var dynSaveBundle = (dynamic)saveBundle;
var entitiesArray = (JArray)dynSaveBundle.entities;
var dynSaveOptions = dynSaveBundle.saveOptions;
SaveOptions = (SaveOptions)JsonSerializer.Deserialize(new JTokenReader(dynSaveOptions), typeof(SaveOptions));
SaveWorkState = new SaveWorkState(this, entitiesArray);
}
SaveChanges now calls that method before continuing on in its previous manner:
public SaveResult SaveChanges(JObject saveBundle, TransactionSettings transactionSettings = null) {
if (SaveWorkState == null || SaveWorkState.WasUsed) {
InitializeSaveState(saveBundle);
}
transactionSettings = transactionSettings ?? BreezeConfig.Instance.GetTransactionSettings();
...
}
Notice that SaveChanges won't call InitializeSaveState twice if you've already prepared the SaveWorkState by, say, calling InitializeSaveState externally and then called SaveChanges immediately thereafter. It also won't save twice with a "used" SaveWorkState.
The source is checked into github right now if you're interested.
You'll be able to get the SaveMap from a save bundle by adding this method to your sub-class of a ContextProvider as in this example:
public class NorthwindContextProvider: EFContextProvider<NorthwindIBContext_CF> {
...
public Dictionary<Type, List<EntityInfo>> GetSaveMapFromSaveBundle(JObject saveBundle) {
InitializeSaveState(saveBundle); // Sets initial EntityInfos
SaveWorkState.BeforeSave(); // Creates the SaveMap as byproduct of BeforeSave logic
return SaveWorkState.SaveMap;
}
...
}
Now you use that as follows:
var saveMap = ContextProvider.GetSaveMapFromSaveBundle(saveBundle);

Is there a crystal reports API in visual studio that I can use to manipulate rpt files using a macro?

I'd like to programmatically manipulate my rpt files using a macro or add-in within Visual Studio 2005. What I want to achieve is the ability to automate updating the custom functions in my reports, since there seems no way to have a single copy of the functions shared between reports.
So I'd like to have a macro to:
Read the function definitions from somewhere, eg an xml file in my project
Open each of the rpt files in my solution and replace the existing function definitions with the new ones.
Is there an API for interacting with the rpt files in this way? Any pointers or examples would be greatly appreciated.
Rory
I think the answer is No, there isn't within VS Crystal Reports. It looks like there's an API for other versions, e.g. this
As an alternative, I've changed to having lots of code in my report formula instead of using custom functions. I can then update the report formula using ReportDocument.DataDefinition.FormulaFields..Text
In my case I only want to update one formula in each report, named 'Period'. I've created a file PeriodFormula.txt and included it in the project with Build Action = EmbeddedResource.
I created this class to read the txt file and update all reports within a given directory. It's currently hardcoded to only update the Period formula, but could easily be modified to operate from a list etc.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Text;
using CrystalDecisions.CrystalReports.Engine;
using CrystalDecisions.Shared;
namespace RMReports
{
public class CustomFunctionUpdater
{
/// <summary>
/// Update all rpt files in the given directory and all subdirectories.
/// Currently only updates the Period formula.
/// </summary>
/// <param name="directoryPath"></param>
public static void UpdateAllReports(String directoryPath)
{
Debug.WriteLine(string.Format("Starting update on all reports within {0}", directoryPath));
const string formulaName = "Period";
int reportsUpdated = 0;
string formulaText = GetFormulaText(formulaName);
foreach (String filename in Directory.GetFiles(directoryPath, "*.rpt", SearchOption.AllDirectories))
{
try
{
if (UpdateReportFunction(filename, formulaName, formulaText))
{
reportsUpdated++;
Debug.WriteLine(string.Format("Updated: {0}", filename));
}
else
Debug.WriteLine(string.Format("No update to: {0}", filename));
}
catch(Exception ex)
{
Debug.WriteLine(string.Format("Failed to update: {0}. Error: {1}", filename, ex.Message));
}
}
Debug.WriteLine(string.Format("done. {0} reports updated", reportsUpdated));
}
/// <summary>
/// Opens the given report file, updates the specified formula with the given text
/// and saves the report.
/// </summary>
/// <param name="reportFilename">The report file to update</param>
/// <param name="formulaName">The name of the formula to update</param>
/// <param name="formulaText">The new text of the formula to update</param>
/// <returns>Whether the report was updated. If the formula doesn't exist this will be false.</returns>
public static bool UpdateReportFunction(String reportFilename, String formulaName, string formulaText)
{
if (String.IsNullOrEmpty(formulaText)) return false;
if (!File.Exists(reportFilename)) throw new FileNotFoundException("reportFilename", reportFilename);
bool updated = false;
ReportDocument document = new ReportDocument();
try
{
document.Load(reportFilename, OpenReportMethod.OpenReportByDefault);
foreach (FormulaFieldDefinition f in document.DataDefinition.FormulaFields)
{
if (f.Name != formulaName) continue;
if (f.Text == formulaText) break; // no update needed
f.Text = formulaText;
updated = true;
break;
}
if (updated)
document.SaveAs(reportFilename);
}
finally
{
if (document.IsLoaded)
document.Close();
}
return updated;
}
public static void UpdateReportFunction(String reportFilename, String formulaName)
{
string formulaText = GetFormulaText(formulaName);
UpdateReportFunction(reportFilename, formulaName, formulaText);
}
/// <summary>
/// Reads the text for the given formula from the current assembly. Assumes the formula
/// exists in a file named [formulaName]Formula.txt that's been compiled as an embedded resource
/// in the current assembly, e.g. DoStuffFormula.txt for a formula named DoStuff.
/// </summary>
/// <param name="formulaName"></param>
/// <returns></returns>
public static String GetFormulaText(String formulaName)
{
string resourceName = Assembly.GetExecutingAssembly().GetName().Name + "." + formulaName + "Formula.txt";
Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName);
if (stream==null) return null;
return (new StreamReader(stream)).ReadToEnd();
}
}
}
Then I use it like this, to update all my reports (which are in folders beneath a 'reports' folder).
DirectoryInfo d = Directory.GetParent(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location));
string reportDirectory = Path.Combine(d.Parent.FullName, "reports");
CustomFunctionUpdater.UpdateAllReports(reportDirectory);
Hopefully someone else finds this useful!

How do I write to the Visual Studio Output Window in My Custom Tool?

I am writing a custom tool and I currently have it doing what I want as far as functionality. I would like to be able to write to Visual Studio if something goes wrong. (Incorrectly formatted code or whatever).
Are there any standards for this? Right now I basically can force the tool to fail and Visual Studio puts in a warning that it has done so. I'd like a category in the Output window with any resulting messages I want to send. I could also live with a more descriptive task/warning in the Error list window.
Output Window
To write to the "General" output window in Visual Studio, you need to do the following:
IVsOutputWindow outWindow = Package.GetGlobalService( typeof( SVsOutputWindow ) ) as IVsOutputWindow;
Guid generalPaneGuid = VSConstants.GUID_OutWindowGeneralPane; // P.S. There's also the GUID_OutWindowDebugPane available.
IVsOutputWindowPane generalPane;
outWindow.GetPane( ref generalPaneGuid , out generalPane );
generalPane.OutputString( "Hello World!" );
generalPane.Activate(); // Brings this pane into view
If, however, you want to write to a custom window, this is what you need to do:
IVsOutputWindow outWindow = Package.GetGlobalService( typeof( SVsOutputWindow ) ) as IVsOutputWindow;
// Use e.g. Tools -> Create GUID to make a stable, but unique GUID for your pane.
// Also, in a real project, this should probably be a static constant, and not a local variable
Guid customGuid = new Guid("0F44E2D1-F5FA-4d2d-AB30-22BE8ECD9789");
string customTitle = "Custom Window Title";
outWindow.CreatePane( ref customGuid, customTitle, 1, 1 );
IVsOutputWindowPane customPane;
outWindow.GetPane( ref customGuid, out customPane);
customPane.OutputString( "Hello, Custom World!" );
customPane.Activate(); // Brings this pane into view
Details on IVsOutputWindow and IVsOutputWindowPane can be found on MSDN.
Error List
For adding items to the error list, the IVsSingleFileGenerator has a method call void Generate(...) which has a parameter of the type IVsGeneratorProgress. This interface has a method void GeneratorError() which lets you report errors and warnings to the Visual Studio error list.
public class MyCodeGenerator : IVsSingleFileGenerator
{
...
public void Generate( string inputFilePath, string inputFileContents, string defaultNamespace, out IntPtr outputFileContents, out int output, IVsGeneratorProgress generateProgress )
{
...
generateProgress.GeneratorError( false, 0, "An error occured", 2, 4);
...
}
...
}
The details of GeneratorError() can be found on MSDN.
There is another way using Marshal.GetActiveObject to grab a running DTE2 instance.
First reference EnvDTE and envdte80. This currently works in VisualStudio 2012, I haven't tried the others yet.
using System;
using System.Runtime.InteropServices;
using EnvDTE;
using EnvDTE80;
internal class VsOutputLogger
{
private static Lazy<Action<string>> _Logger = new Lazy<Action<string>>( () => GetWindow().OutputString );
private static Action<string> Logger
{
get { return _Logger.Value; }
}
public static void SetLogger( Action<string> logger )
{
_Logger = new Lazy<Action<string>>( () => logger );
}
public static void Write( string format, params object[] args)
{
var message = string.Format( format, args );
Write( message );
}
public static void Write( string message )
{
Logger( message + Environment.NewLine );
}
private static OutputWindowPane GetWindow()
{
var dte = (DTE2) Marshal.GetActiveObject( "VisualStudio.DTE" );
return dte.ToolWindows.OutputWindow.ActivePane;
}
}
If you want anything to appear in the Output window, it has to come from stdout. To do this, your app needs to be linked as a "console" app. Set the /SUBSYSTEM:CONSOLE flag in the project's property page, under Linker/System set the SubSystem property to CONSOLE.
Once you have your output in the window, if you include the text "Error:" it will appear as an error, or if you set "Warning:" it will appear as a warning. If your error text begins with a path/filename, followed by a line number in parenthesis, the IDE will recognize it as a "clickable" error, and navigate you automatically to the faulting line.
This is demonstrated in the following helper class from a Microsoft sample project:
https://github.com/microsoft/VSSDK-Extensibility-Samples/blob/df10d37b863feeff6e8fcaa6f4d172f602a882c5/Reference_Services/C%23/Reference.Services/HelperFunctions.cs#L28
The code is as follows:
using System;
using System.Diagnostics;
using Microsoft.VisualStudio.Shell.Interop;
namespace Microsoft.Samples.VisualStudio.Services
{
/// <summary>
/// This class is used to expose some utility functions used in this project.
/// </summary>
internal static class HelperFunctions
{
/// <summary>
/// This function is used to write a string on the Output window of Visual Studio.
/// </summary>
/// <param name="provider">The service provider to query for SVsOutputWindow</param>
/// <param name="text">The text to write</param>
internal static void WriteOnOutputWindow(IServiceProvider provider, string text)
{
// At first write the text on the debug output.
Debug.WriteLine(text);
// Check if we have a provider
if (null == provider)
{
// If there is no provider we can not do anything; exit now.
Debug.WriteLine("No service provider passed to WriteOnOutputWindow.");
return;
}
// Now get the SVsOutputWindow service from the service provider.
IVsOutputWindow outputWindow = provider.GetService(typeof(SVsOutputWindow)) as IVsOutputWindow;
if (null == outputWindow)
{
// If the provider doesn't expose the service there is nothing we can do.
// Write a message on the debug output and exit.
Debug.WriteLine("Can not get the SVsOutputWindow service.");
return;
}
// We can not write on the Output window itself, but only on one of its panes.
// Here we try to use the "General" pane.
Guid guidGeneral = Microsoft.VisualStudio.VSConstants.GUID_OutWindowGeneralPane;
IVsOutputWindowPane windowPane;
if (Microsoft.VisualStudio.ErrorHandler.Failed(outputWindow.GetPane(ref guidGeneral, out windowPane)) ||
(null == windowPane))
{
if (Microsoft.VisualStudio.ErrorHandler.Failed(outputWindow.CreatePane(ref guidGeneral, "General", 1, 0)))
{
// Nothing to do here, just debug output and exit
Debug.WriteLine("Failed to create the Output window pane.");
return;
}
if (Microsoft.VisualStudio.ErrorHandler.Failed(outputWindow.GetPane(ref guidGeneral, out windowPane)) ||
(null == windowPane))
{
// Again, there is nothing we can do to recover from this error, so write on the
// debug output and exit.
Debug.WriteLine("Failed to get the Output window pane.");
return;
}
if (Microsoft.VisualStudio.ErrorHandler.Failed(windowPane.Activate()))
{
Debug.WriteLine("Failed to activate the Output window pane.");
return;
}
}
// Finally we can write on the window pane.
if (Microsoft.VisualStudio.ErrorHandler.Failed(windowPane.OutputString(text)))
{
Debug.WriteLine("Failed to write on the Output window pane.");
}
}
}
}
You can use the Debug and/or Trace classes. There is some information here:
http://msdn.microsoft.com/en-us/library/bs4c1wda(VS.71).aspx
Best of luck.
use System.Diagnostics.Debugger.Message

Resources