Multiple file access abstractions - follow-up - redesign
up vote
0
down vote
favorite
A couple of days ago I asked about an abstraction layer for accessing files (link). There were many great ideas that made me think a lot. The result of it is a complete redesign.
When you think about it, working with files is nothing else but CRUD so by implementing this pattern I was able to solve a lot more problems that I initially intended and the main goal was to have a consistent API for accessing various kinds of resources that must also be dependency-injection-friendly.
This means that I can now create (and already created) abstractions for everything that has some name like:
- AppSettings ✓
- ConnectionStrings ✓
- Physical files ✓
- Physical directories ✓
- Embedded files ✓
- Settings in a database ✓
- Json resource decorator ✓
- Environment variables decorator ✓
- Setting name decorator (for translating Uris to other names) ✓
- Registry
- Ftp
- and what not...
I also think that the best guidelines how to achieve this in such a way that it works for any resource that can be identified by some name are HTTP verbs and up to a certain point REST.
Core
With this in mind I started with the following interface that represents the four main operations as verbs.
[PublicAPI]
public interface IResourceProvider
{
[NotNull]
ResourceMetadata Metadata { get; }
[ItemNotNull]
Task<IResourceInfo> GetAsync([NotNull] UriString uri, ResourceMetadata metadata = null);
[ItemNotNull]
Task<IResourceInfo> PostAsync([NotNull] UriString uri, [NotNull] Stream value, ResourceMetadata metadata = null);
[ItemNotNull]
Task<IResourceInfo> PutAsync([NotNull] UriString uri, [NotNull] Stream value, ResourceMetadata metadata = null);
[ItemNotNull]
Task<IResourceInfo> DeleteAsync([NotNull] UriString uri, ResourceMetadata metadata = null);
}
The IResourceInfo interface is defined as:
[PublicAPI]
public interface IResourceInfo : IEquatable<IResourceInfo>, IEquatable<string>
{
[NotNull]
UriString Uri { get; }
bool Exists { get; }
long? Length { get; }
DateTime? CreatedOn { get; }
DateTime? ModifiedOn { get; }
Task CopyToAsync(Stream stream);
Task<object> DeserializeAsync(Type targetType);
}
As you can see it's using a UriString that a replacement for Uri and that is just a renamed SimpleUri that I asked about here.
The additional property and parameter ResourceMetadata is a DTO for transfering additional information about the request or the provider and is implemented as:
public class ResourceMetadata
{
private readonly IImmutableDictionary<SoftString, object> _metadata;
public ResourceMetadata() : this(ImmutableDictionary<SoftString, object>.Empty) { }
private ResourceMetadata(IImmutableDictionary<SoftString, object> metadata) => _metadata = metadata;
public static ResourceMetadata Empty => new ResourceMetadata();
public object this[SoftString key] => _metadata[key];
public int Count => _metadata.Count;
public IEnumerable<SoftString> Keys => _metadata.Keys;
public IEnumerable<object> Values => _metadata.Values;
public bool ContainsKey(SoftString key) => _metadata.ContainsKey(key);
public bool Contains(KeyValuePair<SoftString, object> pair) => _metadata.Contains(pair);
public bool TryGetKey(SoftString equalKey, out SoftString actualKey) => _metadata.TryGetKey(equalKey, out actualKey);
public bool TryGetValue(SoftString key, out object value) => _metadata.TryGetValue(key, out value);
public ResourceMetadata Add(SoftString key, object value) => new ResourceMetadata(_metadata.Add(key, value));
}
The information that is currently carries is:
public static class ResourceMetadataKeys
{
public static string ProviderDefaultName { get; } = nameof(ProviderDefaultName);
public static string ProviderCustomName { get; } = nameof(ProviderCustomName);
public static string CanGet { get; } = nameof(CanGet);
public static string CanPost { get; } = nameof(CanPost);
public static string CanPut { get; } = nameof(CanPut);
public static string CanDelete { get; } = nameof(CanDelete);
public static string Scheme { get; } = nameof(Scheme);
public static string Serializer { get; } = nameof(Serializer);
}
The last one is specifying how a Stream can be de/serialized and is mainly use by the ResourceHelper and testing:
public static class ResourceHelper
{
public static (Stream Stream, ResourceMetadata Metadata) CreateStream(object value)
{
// Don't dispose streams. The caller takes care of that.
switch (value)
{
case string s:
var streamReader = s.ToStreamReader();
return (streamReader.BaseStream, ResourceMetadata.Empty.Add(Serializer, nameof(StreamReader)));
default:
var binaryFormatter = new BinaryFormatter();
var memoryStream = new MemoryStream();
binaryFormatter.Serialize(memoryStream, value);
return (memoryStream, ResourceMetadata.Empty.Add(Serializer, nameof(BinaryFormatter)));
}
}
public static object CreateObject(Stream stream, ResourceMetadata metadata)
{
if (metadata.TryGetValue(Serializer, out string serializerName))
{
if (serializerName == nameof(BinaryFormatter))
{
var binaryFormatter = new BinaryFormatter();
return binaryFormatter.Deserialize(stream);
}
if (serializerName == nameof(StreamReader))
{
using (var streamReader = new StreamReader(stream))
{
return streamReader.ReadToEnd();
}
}
throw DynamicException.Create("UnsupportedSerializer", $"Cannot deserialize stream because the serializer '{serializerName}' is not supported.");
}
throw DynamicException.Create("SerializerNotFound", $"Serializer wasn't specified.");
}
}
The IResourceProvider has an abstract implementation that I use for adding helper methods:
public abstract class ResourceProvider : IResourceProvider
{
public static readonly string Scheme = "ionymous";
protected ResourceProvider(ResourceMetadata metadata)
{
// If this is a decorator then the decorated resource-provider already has set this.
if (!metadata.ContainsKey(ProviderDefaultName))
{
metadata = metadata.Add(ProviderDefaultName, GetType().ToPrettyString());
}
Metadata = metadata;
}
public virtual ResourceMetadata Metadata { get; }
public abstract Task<IResourceInfo> GetAsync(UriString uri, ResourceMetadata metadata = null);
public virtual Task<IResourceInfo> PostAsync(UriString name, Stream value, ResourceMetadata metadata = null) { throw new NotImplementedException(); }
public abstract Task<IResourceInfo> PutAsync(UriString uri, Stream value, ResourceMetadata metadata = null);
public abstract Task<IResourceInfo> DeleteAsync(UriString uri, ResourceMetadata metadata = null);
protected static Exception CreateException(IResourceProvider provider, string name, ResourceMetadata metadata, Exception inner, [CallerMemberName] string memberName = null)
{
return new Exception();
}
protected UriString ValidateScheme([NotNull] UriString uri, string scheme)
{
if (uri == null) throw new ArgumentNullException(nameof(uri));
return
SoftString.Comparer.Equals(uri.Scheme, scheme)
? uri
: throw DynamicException.Create("InvalidScheme", $"This resource-provider '{GetType().ToPrettyString()}' requires scheme '{scheme}'.");
}
protected UriString ValidateSchemeNotEmpty([NotNull] UriString uri)
{
if (uri == null) throw new ArgumentNullException(nameof(uri));
return
uri.Scheme
? uri
: throw DynamicException.Create("SchemeNotFound", $"Uri '{uri}' does not contain scheme.");
}
}
Implementations
I use this design in several resource-providers so let me post a couple of them.
The first one, is of course the PhysicalFileProvider. Currently it enforces the Scheme to be file and can perform three of the four verbs. (I future, Post is meant to be implemented as append.)
[PublicAPI]
public class PhysicalFileProvider : ResourceProvider
{
public static readonly string Scheme = "file";
public PhysicalFileProvider(ResourceMetadata metadata = null)
: base(
(metadata ?? ResourceMetadata.Empty)
.Add(ResourceMetadataKeys.CanGet, true)
.Add(ResourceMetadataKeys.CanPut, true)
.Add(ResourceMetadataKeys.CanDelete, true)
.Add(ResourceMetadataKeys.Scheme, Scheme)
)
{ }
public override Task<IResourceInfo> GetAsync(UriString uri, ResourceMetadata metadata = null)
{
ValidateScheme(uri, Scheme);
return Task.FromResult<IResourceInfo>(new PhysicalFileInfo(uri));
}
public override async Task<IResourceInfo> PutAsync(UriString uri, Stream value, ResourceMetadata metadata = null)
{
ValidateScheme(uri, Scheme);
try
{
using (var fileStream = new FileStream(uri.Path, FileMode.CreateNew, FileAccess.Write))
{
await value.CopyToAsync(fileStream);
await fileStream.FlushAsync();
}
return await GetAsync(uri, metadata);
}
catch (Exception inner)
{
throw CreateException(this, uri.Path, metadata, inner);
}
}
public override async Task<IResourceInfo> DeleteAsync(UriString uri, ResourceMetadata metadata = null)
{
ValidateScheme(uri, Scheme);
try
{
File.Delete(uri.Path);
return await GetAsync(uri, metadata);
}
catch (Exception inner)
{
throw CreateException(this, uri.Path, metadata, inner);
}
}
}
public static class ResourceProviderExtensions
{
public static Task<IResourceInfo> GetFileInfoAsync(this IResourceProvider resourceProvider, string path, ResourceMetadata metadata = null)
{
return resourceProvider.GetAsync($"file:{path.Replace('\', '/')}", metadata);
}
}
[PublicAPI]
internal class PhysicalFileInfo : ResourceInfo
{
public PhysicalFileInfo([NotNull] UriString uri) : base(uri) { }
public override bool Exists => File.Exists(Uri.Path);
public override long? Length => new FileInfo(Uri.Path).Length;
public override DateTime? CreatedOn => Exists ? File.GetCreationTimeUtc(Uri.Path) : default;
public override DateTime? ModifiedOn => Exists ? File.GetLastWriteTimeUtc(Uri.Path) : default;
public override async Task CopyToAsync(Stream stream)
{
if (Exists)
{
using (var fileStream = File.OpenRead(Uri.Path))
{
await fileStream.CopyToAsync(stream);
}
}
else
{
throw new InvalidOperationException();
}
}
public override async Task<object> DeserializeAsync(Type targetType)
{
if (Exists)
{
using (var fileStream = File.OpenRead(Uri.Path))
using (var streamReader = new StreamReader(fileStream))
{
return await streamReader.ReadToEndAsync();
}
}
else
{
throw new InvalidOperationException();
}
}
}
This is how I implemented the EmbeddedFileProvider. This one can only do Get.
public class EmbeddedFileProvider : ResourceProvider
{
private readonly Assembly _assembly;
public EmbeddedFileProvider([NotNull] Assembly assembly, ResourceMetadata metadata = null)
: base(
(metadata ?? ResourceMetadata.Empty)
.Add(ResourceMetadataKeys.CanGet, true)
)
{
_assembly = assembly ?? throw new ArgumentNullException(nameof(assembly));
var assemblyName = _assembly.GetName().Name.Replace('.', '/');
BaseUri = new UriString($"file:{assemblyName}");
}
public UriString BaseUri { get; }
#region ResourceProvider
public override Task<IResourceInfo> GetAsync(UriString uri, ResourceMetadata metadata = null)
{
ValidateSchemeNotEmpty(uri);
// Embedded resource names are separated by '.' so replace the windows separator.
var fullUri = new UriString(BaseUri, uri.Path.Value);
var fullName = fullUri.Path.Value.Replace('/', '.');
// Embedded resource names are case sensitive so find the actual name of the resource.
var actualName = _assembly.GetManifestResourceNames().FirstOrDefault(name => SoftString.Comparer.Equals(name, fullName));
var getManifestResourceStream = actualName is null ? default(Func<Stream>) : () => _assembly.GetManifestResourceStream(actualName);
return Task.FromResult<IResourceInfo>(new EmbeddedFileInfo(UndoConvertPath(fullName), getManifestResourceStream));
}
public override Task<IResourceInfo> PutAsync(UriString uri, Stream data, ResourceMetadata metadata = null)
{
throw new NotSupportedException($"{nameof(EmbeddedFileProvider)} does not support value serialization.");
}
public override Task<IResourceInfo> DeleteAsync(UriString uri, ResourceMetadata metadata = null)
{
throw new NotSupportedException($"{nameof(EmbeddedFileProvider)} does not support value deletion.");
}
#endregion
// Convert path back to windows format but the last '.' - this is the file extension.
private static string UndoConvertPath(string path) => Regex.Replace(path, @".(?=.*?.)", Path.DirectorySeparatorChar.ToString());
}
internal class EmbeddedFileInfo : ResourceInfo
{
private readonly Func<Stream> _getManifestResourceStream;
public EmbeddedFileInfo(string uri, Func<Stream> getManifestResourceStream) : base(uri)
{
_getManifestResourceStream = getManifestResourceStream;
}
public override bool Exists => !(_getManifestResourceStream is null);
public override long? Length
{
get
{
using (var stream = _getManifestResourceStream?.Invoke())
{
return stream?.Length;
}
}
}
public override DateTime? CreatedOn { get; }
public override DateTime? ModifiedOn { get; }
public override async Task CopyToAsync(Stream stream)
{
if (Exists)
{
using (var resourceStream = _getManifestResourceStream())
{
await resourceStream.CopyToAsync(stream);
}
}
else
{
throw new InvalidOperationException();
}
}
public override async Task<object> DeserializeAsync(Type targetType)
{
using (var resourceStream = _getManifestResourceStream())
using (var streamReader = new StreamReader(resourceStream))
{
return await streamReader.ReadToEndAsync();
}
}
}
And as the last example the AppSettingProvider form another project that is now also built on top of IResourceProvider:
using static ResourceMetadataKeys;
public class AppSettingProvider : ResourceProvider
{
public AppSettingProvider()
: base(
ResourceMetadata.Empty
.Add(CanGet, true)
.Add(CanPut, true)
)
{ }
public override Task<IResourceInfo> GetAsync(UriString uri, ResourceMetadata metadata = null)
{
var settingName = new SettingName(uri);
var exeConfig = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
var actualKey = FindActualKey(exeConfig, settingName) ?? settingName;
var element = exeConfig.AppSettings.Settings[actualKey];
return Task.FromResult<IResourceInfo>(new AppSettingInfo(uri, element?.Value));
}
public override async Task<IResourceInfo> PutAsync(UriString uri, Stream stream, ResourceMetadata metadata = null)
{
using (var valueReader = new StreamReader(stream))
{
var value = await valueReader.ReadToEndAsync();
var settingName = new SettingName(uri);
var exeConfig = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
var actualKey = FindActualKey(exeConfig, settingName) ?? settingName;
var element = exeConfig.AppSettings.Settings[actualKey];
if (element is null)
{
exeConfig.AppSettings.Settings.Add(settingName, (string)value);
}
else
{
exeConfig.AppSettings.Settings[actualKey].Value = (string)value;
}
exeConfig.Save(ConfigurationSaveMode.Minimal);
return await GetAsync(uri);
}
}
public override Task<IResourceInfo> DeleteAsync(UriString uri, ResourceMetadata metadata = null)
{
throw new NotImplementedException();
}
[CanBeNull]
private static string FindActualKey(System.Configuration.Configuration exeConfig, string key)
{
return
exeConfig
.AppSettings
.Settings
.AllKeys
.FirstOrDefault(k => SoftString.Comparer.Equals(k, key));
}
}
internal class AppSettingInfo : ResourceInfo
{
[CanBeNull]
private readonly string _value;
internal AppSettingInfo([NotNull] UriString uri, [CanBeNull] string value) : base(uri)
{
_value = value;
}
public override bool Exists => !(_value is null);
public override long? Length => _value?.Length;
public override DateTime? CreatedOn { get; }
public override DateTime? ModifiedOn { get; }
public override async Task CopyToAsync(Stream stream)
{
if (Exists)
{
// ReSharper disable once AssignNullToNotNullAttribute - this isn't null here
using (var valueStream = _value.ToStreamReader())
{
await valueStream.BaseStream.CopyToAsync(stream);
}
}
}
public override Task<object> DeserializeAsync(Type targetType)
{
return Task.FromResult<object>(_value);
}
}
So, how do you like it and how would you improve it? There are not many properties and some things are hidden inside ResourceMetadata but with a single API for everything you must make some tradeoffs. I find works surprisingly well.
(In future, when I will need for such nichts as Ftp, I will add a new API to the ResourceProvider that I will probably call BeginScope/Session to be able to work with sessions.)
I call it IOnymous and the complete code is as always on GitHub in the experimental branch.
c# api io dependency-injection framework
add a comment |
up vote
0
down vote
favorite
A couple of days ago I asked about an abstraction layer for accessing files (link). There were many great ideas that made me think a lot. The result of it is a complete redesign.
When you think about it, working with files is nothing else but CRUD so by implementing this pattern I was able to solve a lot more problems that I initially intended and the main goal was to have a consistent API for accessing various kinds of resources that must also be dependency-injection-friendly.
This means that I can now create (and already created) abstractions for everything that has some name like:
- AppSettings ✓
- ConnectionStrings ✓
- Physical files ✓
- Physical directories ✓
- Embedded files ✓
- Settings in a database ✓
- Json resource decorator ✓
- Environment variables decorator ✓
- Setting name decorator (for translating Uris to other names) ✓
- Registry
- Ftp
- and what not...
I also think that the best guidelines how to achieve this in such a way that it works for any resource that can be identified by some name are HTTP verbs and up to a certain point REST.
Core
With this in mind I started with the following interface that represents the four main operations as verbs.
[PublicAPI]
public interface IResourceProvider
{
[NotNull]
ResourceMetadata Metadata { get; }
[ItemNotNull]
Task<IResourceInfo> GetAsync([NotNull] UriString uri, ResourceMetadata metadata = null);
[ItemNotNull]
Task<IResourceInfo> PostAsync([NotNull] UriString uri, [NotNull] Stream value, ResourceMetadata metadata = null);
[ItemNotNull]
Task<IResourceInfo> PutAsync([NotNull] UriString uri, [NotNull] Stream value, ResourceMetadata metadata = null);
[ItemNotNull]
Task<IResourceInfo> DeleteAsync([NotNull] UriString uri, ResourceMetadata metadata = null);
}
The IResourceInfo interface is defined as:
[PublicAPI]
public interface IResourceInfo : IEquatable<IResourceInfo>, IEquatable<string>
{
[NotNull]
UriString Uri { get; }
bool Exists { get; }
long? Length { get; }
DateTime? CreatedOn { get; }
DateTime? ModifiedOn { get; }
Task CopyToAsync(Stream stream);
Task<object> DeserializeAsync(Type targetType);
}
As you can see it's using a UriString that a replacement for Uri and that is just a renamed SimpleUri that I asked about here.
The additional property and parameter ResourceMetadata is a DTO for transfering additional information about the request or the provider and is implemented as:
public class ResourceMetadata
{
private readonly IImmutableDictionary<SoftString, object> _metadata;
public ResourceMetadata() : this(ImmutableDictionary<SoftString, object>.Empty) { }
private ResourceMetadata(IImmutableDictionary<SoftString, object> metadata) => _metadata = metadata;
public static ResourceMetadata Empty => new ResourceMetadata();
public object this[SoftString key] => _metadata[key];
public int Count => _metadata.Count;
public IEnumerable<SoftString> Keys => _metadata.Keys;
public IEnumerable<object> Values => _metadata.Values;
public bool ContainsKey(SoftString key) => _metadata.ContainsKey(key);
public bool Contains(KeyValuePair<SoftString, object> pair) => _metadata.Contains(pair);
public bool TryGetKey(SoftString equalKey, out SoftString actualKey) => _metadata.TryGetKey(equalKey, out actualKey);
public bool TryGetValue(SoftString key, out object value) => _metadata.TryGetValue(key, out value);
public ResourceMetadata Add(SoftString key, object value) => new ResourceMetadata(_metadata.Add(key, value));
}
The information that is currently carries is:
public static class ResourceMetadataKeys
{
public static string ProviderDefaultName { get; } = nameof(ProviderDefaultName);
public static string ProviderCustomName { get; } = nameof(ProviderCustomName);
public static string CanGet { get; } = nameof(CanGet);
public static string CanPost { get; } = nameof(CanPost);
public static string CanPut { get; } = nameof(CanPut);
public static string CanDelete { get; } = nameof(CanDelete);
public static string Scheme { get; } = nameof(Scheme);
public static string Serializer { get; } = nameof(Serializer);
}
The last one is specifying how a Stream can be de/serialized and is mainly use by the ResourceHelper and testing:
public static class ResourceHelper
{
public static (Stream Stream, ResourceMetadata Metadata) CreateStream(object value)
{
// Don't dispose streams. The caller takes care of that.
switch (value)
{
case string s:
var streamReader = s.ToStreamReader();
return (streamReader.BaseStream, ResourceMetadata.Empty.Add(Serializer, nameof(StreamReader)));
default:
var binaryFormatter = new BinaryFormatter();
var memoryStream = new MemoryStream();
binaryFormatter.Serialize(memoryStream, value);
return (memoryStream, ResourceMetadata.Empty.Add(Serializer, nameof(BinaryFormatter)));
}
}
public static object CreateObject(Stream stream, ResourceMetadata metadata)
{
if (metadata.TryGetValue(Serializer, out string serializerName))
{
if (serializerName == nameof(BinaryFormatter))
{
var binaryFormatter = new BinaryFormatter();
return binaryFormatter.Deserialize(stream);
}
if (serializerName == nameof(StreamReader))
{
using (var streamReader = new StreamReader(stream))
{
return streamReader.ReadToEnd();
}
}
throw DynamicException.Create("UnsupportedSerializer", $"Cannot deserialize stream because the serializer '{serializerName}' is not supported.");
}
throw DynamicException.Create("SerializerNotFound", $"Serializer wasn't specified.");
}
}
The IResourceProvider has an abstract implementation that I use for adding helper methods:
public abstract class ResourceProvider : IResourceProvider
{
public static readonly string Scheme = "ionymous";
protected ResourceProvider(ResourceMetadata metadata)
{
// If this is a decorator then the decorated resource-provider already has set this.
if (!metadata.ContainsKey(ProviderDefaultName))
{
metadata = metadata.Add(ProviderDefaultName, GetType().ToPrettyString());
}
Metadata = metadata;
}
public virtual ResourceMetadata Metadata { get; }
public abstract Task<IResourceInfo> GetAsync(UriString uri, ResourceMetadata metadata = null);
public virtual Task<IResourceInfo> PostAsync(UriString name, Stream value, ResourceMetadata metadata = null) { throw new NotImplementedException(); }
public abstract Task<IResourceInfo> PutAsync(UriString uri, Stream value, ResourceMetadata metadata = null);
public abstract Task<IResourceInfo> DeleteAsync(UriString uri, ResourceMetadata metadata = null);
protected static Exception CreateException(IResourceProvider provider, string name, ResourceMetadata metadata, Exception inner, [CallerMemberName] string memberName = null)
{
return new Exception();
}
protected UriString ValidateScheme([NotNull] UriString uri, string scheme)
{
if (uri == null) throw new ArgumentNullException(nameof(uri));
return
SoftString.Comparer.Equals(uri.Scheme, scheme)
? uri
: throw DynamicException.Create("InvalidScheme", $"This resource-provider '{GetType().ToPrettyString()}' requires scheme '{scheme}'.");
}
protected UriString ValidateSchemeNotEmpty([NotNull] UriString uri)
{
if (uri == null) throw new ArgumentNullException(nameof(uri));
return
uri.Scheme
? uri
: throw DynamicException.Create("SchemeNotFound", $"Uri '{uri}' does not contain scheme.");
}
}
Implementations
I use this design in several resource-providers so let me post a couple of them.
The first one, is of course the PhysicalFileProvider. Currently it enforces the Scheme to be file and can perform three of the four verbs. (I future, Post is meant to be implemented as append.)
[PublicAPI]
public class PhysicalFileProvider : ResourceProvider
{
public static readonly string Scheme = "file";
public PhysicalFileProvider(ResourceMetadata metadata = null)
: base(
(metadata ?? ResourceMetadata.Empty)
.Add(ResourceMetadataKeys.CanGet, true)
.Add(ResourceMetadataKeys.CanPut, true)
.Add(ResourceMetadataKeys.CanDelete, true)
.Add(ResourceMetadataKeys.Scheme, Scheme)
)
{ }
public override Task<IResourceInfo> GetAsync(UriString uri, ResourceMetadata metadata = null)
{
ValidateScheme(uri, Scheme);
return Task.FromResult<IResourceInfo>(new PhysicalFileInfo(uri));
}
public override async Task<IResourceInfo> PutAsync(UriString uri, Stream value, ResourceMetadata metadata = null)
{
ValidateScheme(uri, Scheme);
try
{
using (var fileStream = new FileStream(uri.Path, FileMode.CreateNew, FileAccess.Write))
{
await value.CopyToAsync(fileStream);
await fileStream.FlushAsync();
}
return await GetAsync(uri, metadata);
}
catch (Exception inner)
{
throw CreateException(this, uri.Path, metadata, inner);
}
}
public override async Task<IResourceInfo> DeleteAsync(UriString uri, ResourceMetadata metadata = null)
{
ValidateScheme(uri, Scheme);
try
{
File.Delete(uri.Path);
return await GetAsync(uri, metadata);
}
catch (Exception inner)
{
throw CreateException(this, uri.Path, metadata, inner);
}
}
}
public static class ResourceProviderExtensions
{
public static Task<IResourceInfo> GetFileInfoAsync(this IResourceProvider resourceProvider, string path, ResourceMetadata metadata = null)
{
return resourceProvider.GetAsync($"file:{path.Replace('\', '/')}", metadata);
}
}
[PublicAPI]
internal class PhysicalFileInfo : ResourceInfo
{
public PhysicalFileInfo([NotNull] UriString uri) : base(uri) { }
public override bool Exists => File.Exists(Uri.Path);
public override long? Length => new FileInfo(Uri.Path).Length;
public override DateTime? CreatedOn => Exists ? File.GetCreationTimeUtc(Uri.Path) : default;
public override DateTime? ModifiedOn => Exists ? File.GetLastWriteTimeUtc(Uri.Path) : default;
public override async Task CopyToAsync(Stream stream)
{
if (Exists)
{
using (var fileStream = File.OpenRead(Uri.Path))
{
await fileStream.CopyToAsync(stream);
}
}
else
{
throw new InvalidOperationException();
}
}
public override async Task<object> DeserializeAsync(Type targetType)
{
if (Exists)
{
using (var fileStream = File.OpenRead(Uri.Path))
using (var streamReader = new StreamReader(fileStream))
{
return await streamReader.ReadToEndAsync();
}
}
else
{
throw new InvalidOperationException();
}
}
}
This is how I implemented the EmbeddedFileProvider. This one can only do Get.
public class EmbeddedFileProvider : ResourceProvider
{
private readonly Assembly _assembly;
public EmbeddedFileProvider([NotNull] Assembly assembly, ResourceMetadata metadata = null)
: base(
(metadata ?? ResourceMetadata.Empty)
.Add(ResourceMetadataKeys.CanGet, true)
)
{
_assembly = assembly ?? throw new ArgumentNullException(nameof(assembly));
var assemblyName = _assembly.GetName().Name.Replace('.', '/');
BaseUri = new UriString($"file:{assemblyName}");
}
public UriString BaseUri { get; }
#region ResourceProvider
public override Task<IResourceInfo> GetAsync(UriString uri, ResourceMetadata metadata = null)
{
ValidateSchemeNotEmpty(uri);
// Embedded resource names are separated by '.' so replace the windows separator.
var fullUri = new UriString(BaseUri, uri.Path.Value);
var fullName = fullUri.Path.Value.Replace('/', '.');
// Embedded resource names are case sensitive so find the actual name of the resource.
var actualName = _assembly.GetManifestResourceNames().FirstOrDefault(name => SoftString.Comparer.Equals(name, fullName));
var getManifestResourceStream = actualName is null ? default(Func<Stream>) : () => _assembly.GetManifestResourceStream(actualName);
return Task.FromResult<IResourceInfo>(new EmbeddedFileInfo(UndoConvertPath(fullName), getManifestResourceStream));
}
public override Task<IResourceInfo> PutAsync(UriString uri, Stream data, ResourceMetadata metadata = null)
{
throw new NotSupportedException($"{nameof(EmbeddedFileProvider)} does not support value serialization.");
}
public override Task<IResourceInfo> DeleteAsync(UriString uri, ResourceMetadata metadata = null)
{
throw new NotSupportedException($"{nameof(EmbeddedFileProvider)} does not support value deletion.");
}
#endregion
// Convert path back to windows format but the last '.' - this is the file extension.
private static string UndoConvertPath(string path) => Regex.Replace(path, @".(?=.*?.)", Path.DirectorySeparatorChar.ToString());
}
internal class EmbeddedFileInfo : ResourceInfo
{
private readonly Func<Stream> _getManifestResourceStream;
public EmbeddedFileInfo(string uri, Func<Stream> getManifestResourceStream) : base(uri)
{
_getManifestResourceStream = getManifestResourceStream;
}
public override bool Exists => !(_getManifestResourceStream is null);
public override long? Length
{
get
{
using (var stream = _getManifestResourceStream?.Invoke())
{
return stream?.Length;
}
}
}
public override DateTime? CreatedOn { get; }
public override DateTime? ModifiedOn { get; }
public override async Task CopyToAsync(Stream stream)
{
if (Exists)
{
using (var resourceStream = _getManifestResourceStream())
{
await resourceStream.CopyToAsync(stream);
}
}
else
{
throw new InvalidOperationException();
}
}
public override async Task<object> DeserializeAsync(Type targetType)
{
using (var resourceStream = _getManifestResourceStream())
using (var streamReader = new StreamReader(resourceStream))
{
return await streamReader.ReadToEndAsync();
}
}
}
And as the last example the AppSettingProvider form another project that is now also built on top of IResourceProvider:
using static ResourceMetadataKeys;
public class AppSettingProvider : ResourceProvider
{
public AppSettingProvider()
: base(
ResourceMetadata.Empty
.Add(CanGet, true)
.Add(CanPut, true)
)
{ }
public override Task<IResourceInfo> GetAsync(UriString uri, ResourceMetadata metadata = null)
{
var settingName = new SettingName(uri);
var exeConfig = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
var actualKey = FindActualKey(exeConfig, settingName) ?? settingName;
var element = exeConfig.AppSettings.Settings[actualKey];
return Task.FromResult<IResourceInfo>(new AppSettingInfo(uri, element?.Value));
}
public override async Task<IResourceInfo> PutAsync(UriString uri, Stream stream, ResourceMetadata metadata = null)
{
using (var valueReader = new StreamReader(stream))
{
var value = await valueReader.ReadToEndAsync();
var settingName = new SettingName(uri);
var exeConfig = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
var actualKey = FindActualKey(exeConfig, settingName) ?? settingName;
var element = exeConfig.AppSettings.Settings[actualKey];
if (element is null)
{
exeConfig.AppSettings.Settings.Add(settingName, (string)value);
}
else
{
exeConfig.AppSettings.Settings[actualKey].Value = (string)value;
}
exeConfig.Save(ConfigurationSaveMode.Minimal);
return await GetAsync(uri);
}
}
public override Task<IResourceInfo> DeleteAsync(UriString uri, ResourceMetadata metadata = null)
{
throw new NotImplementedException();
}
[CanBeNull]
private static string FindActualKey(System.Configuration.Configuration exeConfig, string key)
{
return
exeConfig
.AppSettings
.Settings
.AllKeys
.FirstOrDefault(k => SoftString.Comparer.Equals(k, key));
}
}
internal class AppSettingInfo : ResourceInfo
{
[CanBeNull]
private readonly string _value;
internal AppSettingInfo([NotNull] UriString uri, [CanBeNull] string value) : base(uri)
{
_value = value;
}
public override bool Exists => !(_value is null);
public override long? Length => _value?.Length;
public override DateTime? CreatedOn { get; }
public override DateTime? ModifiedOn { get; }
public override async Task CopyToAsync(Stream stream)
{
if (Exists)
{
// ReSharper disable once AssignNullToNotNullAttribute - this isn't null here
using (var valueStream = _value.ToStreamReader())
{
await valueStream.BaseStream.CopyToAsync(stream);
}
}
}
public override Task<object> DeserializeAsync(Type targetType)
{
return Task.FromResult<object>(_value);
}
}
So, how do you like it and how would you improve it? There are not many properties and some things are hidden inside ResourceMetadata but with a single API for everything you must make some tradeoffs. I find works surprisingly well.
(In future, when I will need for such nichts as Ftp, I will add a new API to the ResourceProvider that I will probably call BeginScope/Session to be able to work with sessions.)
I call it IOnymous and the complete code is as always on GitHub in the experimental branch.
c# api io dependency-injection framework
add a comment |
up vote
0
down vote
favorite
up vote
0
down vote
favorite
A couple of days ago I asked about an abstraction layer for accessing files (link). There were many great ideas that made me think a lot. The result of it is a complete redesign.
When you think about it, working with files is nothing else but CRUD so by implementing this pattern I was able to solve a lot more problems that I initially intended and the main goal was to have a consistent API for accessing various kinds of resources that must also be dependency-injection-friendly.
This means that I can now create (and already created) abstractions for everything that has some name like:
- AppSettings ✓
- ConnectionStrings ✓
- Physical files ✓
- Physical directories ✓
- Embedded files ✓
- Settings in a database ✓
- Json resource decorator ✓
- Environment variables decorator ✓
- Setting name decorator (for translating Uris to other names) ✓
- Registry
- Ftp
- and what not...
I also think that the best guidelines how to achieve this in such a way that it works for any resource that can be identified by some name are HTTP verbs and up to a certain point REST.
Core
With this in mind I started with the following interface that represents the four main operations as verbs.
[PublicAPI]
public interface IResourceProvider
{
[NotNull]
ResourceMetadata Metadata { get; }
[ItemNotNull]
Task<IResourceInfo> GetAsync([NotNull] UriString uri, ResourceMetadata metadata = null);
[ItemNotNull]
Task<IResourceInfo> PostAsync([NotNull] UriString uri, [NotNull] Stream value, ResourceMetadata metadata = null);
[ItemNotNull]
Task<IResourceInfo> PutAsync([NotNull] UriString uri, [NotNull] Stream value, ResourceMetadata metadata = null);
[ItemNotNull]
Task<IResourceInfo> DeleteAsync([NotNull] UriString uri, ResourceMetadata metadata = null);
}
The IResourceInfo interface is defined as:
[PublicAPI]
public interface IResourceInfo : IEquatable<IResourceInfo>, IEquatable<string>
{
[NotNull]
UriString Uri { get; }
bool Exists { get; }
long? Length { get; }
DateTime? CreatedOn { get; }
DateTime? ModifiedOn { get; }
Task CopyToAsync(Stream stream);
Task<object> DeserializeAsync(Type targetType);
}
As you can see it's using a UriString that a replacement for Uri and that is just a renamed SimpleUri that I asked about here.
The additional property and parameter ResourceMetadata is a DTO for transfering additional information about the request or the provider and is implemented as:
public class ResourceMetadata
{
private readonly IImmutableDictionary<SoftString, object> _metadata;
public ResourceMetadata() : this(ImmutableDictionary<SoftString, object>.Empty) { }
private ResourceMetadata(IImmutableDictionary<SoftString, object> metadata) => _metadata = metadata;
public static ResourceMetadata Empty => new ResourceMetadata();
public object this[SoftString key] => _metadata[key];
public int Count => _metadata.Count;
public IEnumerable<SoftString> Keys => _metadata.Keys;
public IEnumerable<object> Values => _metadata.Values;
public bool ContainsKey(SoftString key) => _metadata.ContainsKey(key);
public bool Contains(KeyValuePair<SoftString, object> pair) => _metadata.Contains(pair);
public bool TryGetKey(SoftString equalKey, out SoftString actualKey) => _metadata.TryGetKey(equalKey, out actualKey);
public bool TryGetValue(SoftString key, out object value) => _metadata.TryGetValue(key, out value);
public ResourceMetadata Add(SoftString key, object value) => new ResourceMetadata(_metadata.Add(key, value));
}
The information that is currently carries is:
public static class ResourceMetadataKeys
{
public static string ProviderDefaultName { get; } = nameof(ProviderDefaultName);
public static string ProviderCustomName { get; } = nameof(ProviderCustomName);
public static string CanGet { get; } = nameof(CanGet);
public static string CanPost { get; } = nameof(CanPost);
public static string CanPut { get; } = nameof(CanPut);
public static string CanDelete { get; } = nameof(CanDelete);
public static string Scheme { get; } = nameof(Scheme);
public static string Serializer { get; } = nameof(Serializer);
}
The last one is specifying how a Stream can be de/serialized and is mainly use by the ResourceHelper and testing:
public static class ResourceHelper
{
public static (Stream Stream, ResourceMetadata Metadata) CreateStream(object value)
{
// Don't dispose streams. The caller takes care of that.
switch (value)
{
case string s:
var streamReader = s.ToStreamReader();
return (streamReader.BaseStream, ResourceMetadata.Empty.Add(Serializer, nameof(StreamReader)));
default:
var binaryFormatter = new BinaryFormatter();
var memoryStream = new MemoryStream();
binaryFormatter.Serialize(memoryStream, value);
return (memoryStream, ResourceMetadata.Empty.Add(Serializer, nameof(BinaryFormatter)));
}
}
public static object CreateObject(Stream stream, ResourceMetadata metadata)
{
if (metadata.TryGetValue(Serializer, out string serializerName))
{
if (serializerName == nameof(BinaryFormatter))
{
var binaryFormatter = new BinaryFormatter();
return binaryFormatter.Deserialize(stream);
}
if (serializerName == nameof(StreamReader))
{
using (var streamReader = new StreamReader(stream))
{
return streamReader.ReadToEnd();
}
}
throw DynamicException.Create("UnsupportedSerializer", $"Cannot deserialize stream because the serializer '{serializerName}' is not supported.");
}
throw DynamicException.Create("SerializerNotFound", $"Serializer wasn't specified.");
}
}
The IResourceProvider has an abstract implementation that I use for adding helper methods:
public abstract class ResourceProvider : IResourceProvider
{
public static readonly string Scheme = "ionymous";
protected ResourceProvider(ResourceMetadata metadata)
{
// If this is a decorator then the decorated resource-provider already has set this.
if (!metadata.ContainsKey(ProviderDefaultName))
{
metadata = metadata.Add(ProviderDefaultName, GetType().ToPrettyString());
}
Metadata = metadata;
}
public virtual ResourceMetadata Metadata { get; }
public abstract Task<IResourceInfo> GetAsync(UriString uri, ResourceMetadata metadata = null);
public virtual Task<IResourceInfo> PostAsync(UriString name, Stream value, ResourceMetadata metadata = null) { throw new NotImplementedException(); }
public abstract Task<IResourceInfo> PutAsync(UriString uri, Stream value, ResourceMetadata metadata = null);
public abstract Task<IResourceInfo> DeleteAsync(UriString uri, ResourceMetadata metadata = null);
protected static Exception CreateException(IResourceProvider provider, string name, ResourceMetadata metadata, Exception inner, [CallerMemberName] string memberName = null)
{
return new Exception();
}
protected UriString ValidateScheme([NotNull] UriString uri, string scheme)
{
if (uri == null) throw new ArgumentNullException(nameof(uri));
return
SoftString.Comparer.Equals(uri.Scheme, scheme)
? uri
: throw DynamicException.Create("InvalidScheme", $"This resource-provider '{GetType().ToPrettyString()}' requires scheme '{scheme}'.");
}
protected UriString ValidateSchemeNotEmpty([NotNull] UriString uri)
{
if (uri == null) throw new ArgumentNullException(nameof(uri));
return
uri.Scheme
? uri
: throw DynamicException.Create("SchemeNotFound", $"Uri '{uri}' does not contain scheme.");
}
}
Implementations
I use this design in several resource-providers so let me post a couple of them.
The first one, is of course the PhysicalFileProvider. Currently it enforces the Scheme to be file and can perform three of the four verbs. (I future, Post is meant to be implemented as append.)
[PublicAPI]
public class PhysicalFileProvider : ResourceProvider
{
public static readonly string Scheme = "file";
public PhysicalFileProvider(ResourceMetadata metadata = null)
: base(
(metadata ?? ResourceMetadata.Empty)
.Add(ResourceMetadataKeys.CanGet, true)
.Add(ResourceMetadataKeys.CanPut, true)
.Add(ResourceMetadataKeys.CanDelete, true)
.Add(ResourceMetadataKeys.Scheme, Scheme)
)
{ }
public override Task<IResourceInfo> GetAsync(UriString uri, ResourceMetadata metadata = null)
{
ValidateScheme(uri, Scheme);
return Task.FromResult<IResourceInfo>(new PhysicalFileInfo(uri));
}
public override async Task<IResourceInfo> PutAsync(UriString uri, Stream value, ResourceMetadata metadata = null)
{
ValidateScheme(uri, Scheme);
try
{
using (var fileStream = new FileStream(uri.Path, FileMode.CreateNew, FileAccess.Write))
{
await value.CopyToAsync(fileStream);
await fileStream.FlushAsync();
}
return await GetAsync(uri, metadata);
}
catch (Exception inner)
{
throw CreateException(this, uri.Path, metadata, inner);
}
}
public override async Task<IResourceInfo> DeleteAsync(UriString uri, ResourceMetadata metadata = null)
{
ValidateScheme(uri, Scheme);
try
{
File.Delete(uri.Path);
return await GetAsync(uri, metadata);
}
catch (Exception inner)
{
throw CreateException(this, uri.Path, metadata, inner);
}
}
}
public static class ResourceProviderExtensions
{
public static Task<IResourceInfo> GetFileInfoAsync(this IResourceProvider resourceProvider, string path, ResourceMetadata metadata = null)
{
return resourceProvider.GetAsync($"file:{path.Replace('\', '/')}", metadata);
}
}
[PublicAPI]
internal class PhysicalFileInfo : ResourceInfo
{
public PhysicalFileInfo([NotNull] UriString uri) : base(uri) { }
public override bool Exists => File.Exists(Uri.Path);
public override long? Length => new FileInfo(Uri.Path).Length;
public override DateTime? CreatedOn => Exists ? File.GetCreationTimeUtc(Uri.Path) : default;
public override DateTime? ModifiedOn => Exists ? File.GetLastWriteTimeUtc(Uri.Path) : default;
public override async Task CopyToAsync(Stream stream)
{
if (Exists)
{
using (var fileStream = File.OpenRead(Uri.Path))
{
await fileStream.CopyToAsync(stream);
}
}
else
{
throw new InvalidOperationException();
}
}
public override async Task<object> DeserializeAsync(Type targetType)
{
if (Exists)
{
using (var fileStream = File.OpenRead(Uri.Path))
using (var streamReader = new StreamReader(fileStream))
{
return await streamReader.ReadToEndAsync();
}
}
else
{
throw new InvalidOperationException();
}
}
}
This is how I implemented the EmbeddedFileProvider. This one can only do Get.
public class EmbeddedFileProvider : ResourceProvider
{
private readonly Assembly _assembly;
public EmbeddedFileProvider([NotNull] Assembly assembly, ResourceMetadata metadata = null)
: base(
(metadata ?? ResourceMetadata.Empty)
.Add(ResourceMetadataKeys.CanGet, true)
)
{
_assembly = assembly ?? throw new ArgumentNullException(nameof(assembly));
var assemblyName = _assembly.GetName().Name.Replace('.', '/');
BaseUri = new UriString($"file:{assemblyName}");
}
public UriString BaseUri { get; }
#region ResourceProvider
public override Task<IResourceInfo> GetAsync(UriString uri, ResourceMetadata metadata = null)
{
ValidateSchemeNotEmpty(uri);
// Embedded resource names are separated by '.' so replace the windows separator.
var fullUri = new UriString(BaseUri, uri.Path.Value);
var fullName = fullUri.Path.Value.Replace('/', '.');
// Embedded resource names are case sensitive so find the actual name of the resource.
var actualName = _assembly.GetManifestResourceNames().FirstOrDefault(name => SoftString.Comparer.Equals(name, fullName));
var getManifestResourceStream = actualName is null ? default(Func<Stream>) : () => _assembly.GetManifestResourceStream(actualName);
return Task.FromResult<IResourceInfo>(new EmbeddedFileInfo(UndoConvertPath(fullName), getManifestResourceStream));
}
public override Task<IResourceInfo> PutAsync(UriString uri, Stream data, ResourceMetadata metadata = null)
{
throw new NotSupportedException($"{nameof(EmbeddedFileProvider)} does not support value serialization.");
}
public override Task<IResourceInfo> DeleteAsync(UriString uri, ResourceMetadata metadata = null)
{
throw new NotSupportedException($"{nameof(EmbeddedFileProvider)} does not support value deletion.");
}
#endregion
// Convert path back to windows format but the last '.' - this is the file extension.
private static string UndoConvertPath(string path) => Regex.Replace(path, @".(?=.*?.)", Path.DirectorySeparatorChar.ToString());
}
internal class EmbeddedFileInfo : ResourceInfo
{
private readonly Func<Stream> _getManifestResourceStream;
public EmbeddedFileInfo(string uri, Func<Stream> getManifestResourceStream) : base(uri)
{
_getManifestResourceStream = getManifestResourceStream;
}
public override bool Exists => !(_getManifestResourceStream is null);
public override long? Length
{
get
{
using (var stream = _getManifestResourceStream?.Invoke())
{
return stream?.Length;
}
}
}
public override DateTime? CreatedOn { get; }
public override DateTime? ModifiedOn { get; }
public override async Task CopyToAsync(Stream stream)
{
if (Exists)
{
using (var resourceStream = _getManifestResourceStream())
{
await resourceStream.CopyToAsync(stream);
}
}
else
{
throw new InvalidOperationException();
}
}
public override async Task<object> DeserializeAsync(Type targetType)
{
using (var resourceStream = _getManifestResourceStream())
using (var streamReader = new StreamReader(resourceStream))
{
return await streamReader.ReadToEndAsync();
}
}
}
And as the last example the AppSettingProvider form another project that is now also built on top of IResourceProvider:
using static ResourceMetadataKeys;
public class AppSettingProvider : ResourceProvider
{
public AppSettingProvider()
: base(
ResourceMetadata.Empty
.Add(CanGet, true)
.Add(CanPut, true)
)
{ }
public override Task<IResourceInfo> GetAsync(UriString uri, ResourceMetadata metadata = null)
{
var settingName = new SettingName(uri);
var exeConfig = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
var actualKey = FindActualKey(exeConfig, settingName) ?? settingName;
var element = exeConfig.AppSettings.Settings[actualKey];
return Task.FromResult<IResourceInfo>(new AppSettingInfo(uri, element?.Value));
}
public override async Task<IResourceInfo> PutAsync(UriString uri, Stream stream, ResourceMetadata metadata = null)
{
using (var valueReader = new StreamReader(stream))
{
var value = await valueReader.ReadToEndAsync();
var settingName = new SettingName(uri);
var exeConfig = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
var actualKey = FindActualKey(exeConfig, settingName) ?? settingName;
var element = exeConfig.AppSettings.Settings[actualKey];
if (element is null)
{
exeConfig.AppSettings.Settings.Add(settingName, (string)value);
}
else
{
exeConfig.AppSettings.Settings[actualKey].Value = (string)value;
}
exeConfig.Save(ConfigurationSaveMode.Minimal);
return await GetAsync(uri);
}
}
public override Task<IResourceInfo> DeleteAsync(UriString uri, ResourceMetadata metadata = null)
{
throw new NotImplementedException();
}
[CanBeNull]
private static string FindActualKey(System.Configuration.Configuration exeConfig, string key)
{
return
exeConfig
.AppSettings
.Settings
.AllKeys
.FirstOrDefault(k => SoftString.Comparer.Equals(k, key));
}
}
internal class AppSettingInfo : ResourceInfo
{
[CanBeNull]
private readonly string _value;
internal AppSettingInfo([NotNull] UriString uri, [CanBeNull] string value) : base(uri)
{
_value = value;
}
public override bool Exists => !(_value is null);
public override long? Length => _value?.Length;
public override DateTime? CreatedOn { get; }
public override DateTime? ModifiedOn { get; }
public override async Task CopyToAsync(Stream stream)
{
if (Exists)
{
// ReSharper disable once AssignNullToNotNullAttribute - this isn't null here
using (var valueStream = _value.ToStreamReader())
{
await valueStream.BaseStream.CopyToAsync(stream);
}
}
}
public override Task<object> DeserializeAsync(Type targetType)
{
return Task.FromResult<object>(_value);
}
}
So, how do you like it and how would you improve it? There are not many properties and some things are hidden inside ResourceMetadata but with a single API for everything you must make some tradeoffs. I find works surprisingly well.
(In future, when I will need for such nichts as Ftp, I will add a new API to the ResourceProvider that I will probably call BeginScope/Session to be able to work with sessions.)
I call it IOnymous and the complete code is as always on GitHub in the experimental branch.
c# api io dependency-injection framework
A couple of days ago I asked about an abstraction layer for accessing files (link). There were many great ideas that made me think a lot. The result of it is a complete redesign.
When you think about it, working with files is nothing else but CRUD so by implementing this pattern I was able to solve a lot more problems that I initially intended and the main goal was to have a consistent API for accessing various kinds of resources that must also be dependency-injection-friendly.
This means that I can now create (and already created) abstractions for everything that has some name like:
- AppSettings ✓
- ConnectionStrings ✓
- Physical files ✓
- Physical directories ✓
- Embedded files ✓
- Settings in a database ✓
- Json resource decorator ✓
- Environment variables decorator ✓
- Setting name decorator (for translating Uris to other names) ✓
- Registry
- Ftp
- and what not...
I also think that the best guidelines how to achieve this in such a way that it works for any resource that can be identified by some name are HTTP verbs and up to a certain point REST.
Core
With this in mind I started with the following interface that represents the four main operations as verbs.
[PublicAPI]
public interface IResourceProvider
{
[NotNull]
ResourceMetadata Metadata { get; }
[ItemNotNull]
Task<IResourceInfo> GetAsync([NotNull] UriString uri, ResourceMetadata metadata = null);
[ItemNotNull]
Task<IResourceInfo> PostAsync([NotNull] UriString uri, [NotNull] Stream value, ResourceMetadata metadata = null);
[ItemNotNull]
Task<IResourceInfo> PutAsync([NotNull] UriString uri, [NotNull] Stream value, ResourceMetadata metadata = null);
[ItemNotNull]
Task<IResourceInfo> DeleteAsync([NotNull] UriString uri, ResourceMetadata metadata = null);
}
The IResourceInfo interface is defined as:
[PublicAPI]
public interface IResourceInfo : IEquatable<IResourceInfo>, IEquatable<string>
{
[NotNull]
UriString Uri { get; }
bool Exists { get; }
long? Length { get; }
DateTime? CreatedOn { get; }
DateTime? ModifiedOn { get; }
Task CopyToAsync(Stream stream);
Task<object> DeserializeAsync(Type targetType);
}
As you can see it's using a UriString that a replacement for Uri and that is just a renamed SimpleUri that I asked about here.
The additional property and parameter ResourceMetadata is a DTO for transfering additional information about the request or the provider and is implemented as:
public class ResourceMetadata
{
private readonly IImmutableDictionary<SoftString, object> _metadata;
public ResourceMetadata() : this(ImmutableDictionary<SoftString, object>.Empty) { }
private ResourceMetadata(IImmutableDictionary<SoftString, object> metadata) => _metadata = metadata;
public static ResourceMetadata Empty => new ResourceMetadata();
public object this[SoftString key] => _metadata[key];
public int Count => _metadata.Count;
public IEnumerable<SoftString> Keys => _metadata.Keys;
public IEnumerable<object> Values => _metadata.Values;
public bool ContainsKey(SoftString key) => _metadata.ContainsKey(key);
public bool Contains(KeyValuePair<SoftString, object> pair) => _metadata.Contains(pair);
public bool TryGetKey(SoftString equalKey, out SoftString actualKey) => _metadata.TryGetKey(equalKey, out actualKey);
public bool TryGetValue(SoftString key, out object value) => _metadata.TryGetValue(key, out value);
public ResourceMetadata Add(SoftString key, object value) => new ResourceMetadata(_metadata.Add(key, value));
}
The information that is currently carries is:
public static class ResourceMetadataKeys
{
public static string ProviderDefaultName { get; } = nameof(ProviderDefaultName);
public static string ProviderCustomName { get; } = nameof(ProviderCustomName);
public static string CanGet { get; } = nameof(CanGet);
public static string CanPost { get; } = nameof(CanPost);
public static string CanPut { get; } = nameof(CanPut);
public static string CanDelete { get; } = nameof(CanDelete);
public static string Scheme { get; } = nameof(Scheme);
public static string Serializer { get; } = nameof(Serializer);
}
The last one is specifying how a Stream can be de/serialized and is mainly use by the ResourceHelper and testing:
public static class ResourceHelper
{
public static (Stream Stream, ResourceMetadata Metadata) CreateStream(object value)
{
// Don't dispose streams. The caller takes care of that.
switch (value)
{
case string s:
var streamReader = s.ToStreamReader();
return (streamReader.BaseStream, ResourceMetadata.Empty.Add(Serializer, nameof(StreamReader)));
default:
var binaryFormatter = new BinaryFormatter();
var memoryStream = new MemoryStream();
binaryFormatter.Serialize(memoryStream, value);
return (memoryStream, ResourceMetadata.Empty.Add(Serializer, nameof(BinaryFormatter)));
}
}
public static object CreateObject(Stream stream, ResourceMetadata metadata)
{
if (metadata.TryGetValue(Serializer, out string serializerName))
{
if (serializerName == nameof(BinaryFormatter))
{
var binaryFormatter = new BinaryFormatter();
return binaryFormatter.Deserialize(stream);
}
if (serializerName == nameof(StreamReader))
{
using (var streamReader = new StreamReader(stream))
{
return streamReader.ReadToEnd();
}
}
throw DynamicException.Create("UnsupportedSerializer", $"Cannot deserialize stream because the serializer '{serializerName}' is not supported.");
}
throw DynamicException.Create("SerializerNotFound", $"Serializer wasn't specified.");
}
}
The IResourceProvider has an abstract implementation that I use for adding helper methods:
public abstract class ResourceProvider : IResourceProvider
{
public static readonly string Scheme = "ionymous";
protected ResourceProvider(ResourceMetadata metadata)
{
// If this is a decorator then the decorated resource-provider already has set this.
if (!metadata.ContainsKey(ProviderDefaultName))
{
metadata = metadata.Add(ProviderDefaultName, GetType().ToPrettyString());
}
Metadata = metadata;
}
public virtual ResourceMetadata Metadata { get; }
public abstract Task<IResourceInfo> GetAsync(UriString uri, ResourceMetadata metadata = null);
public virtual Task<IResourceInfo> PostAsync(UriString name, Stream value, ResourceMetadata metadata = null) { throw new NotImplementedException(); }
public abstract Task<IResourceInfo> PutAsync(UriString uri, Stream value, ResourceMetadata metadata = null);
public abstract Task<IResourceInfo> DeleteAsync(UriString uri, ResourceMetadata metadata = null);
protected static Exception CreateException(IResourceProvider provider, string name, ResourceMetadata metadata, Exception inner, [CallerMemberName] string memberName = null)
{
return new Exception();
}
protected UriString ValidateScheme([NotNull] UriString uri, string scheme)
{
if (uri == null) throw new ArgumentNullException(nameof(uri));
return
SoftString.Comparer.Equals(uri.Scheme, scheme)
? uri
: throw DynamicException.Create("InvalidScheme", $"This resource-provider '{GetType().ToPrettyString()}' requires scheme '{scheme}'.");
}
protected UriString ValidateSchemeNotEmpty([NotNull] UriString uri)
{
if (uri == null) throw new ArgumentNullException(nameof(uri));
return
uri.Scheme
? uri
: throw DynamicException.Create("SchemeNotFound", $"Uri '{uri}' does not contain scheme.");
}
}
Implementations
I use this design in several resource-providers so let me post a couple of them.
The first one, is of course the PhysicalFileProvider. Currently it enforces the Scheme to be file and can perform three of the four verbs. (I future, Post is meant to be implemented as append.)
[PublicAPI]
public class PhysicalFileProvider : ResourceProvider
{
public static readonly string Scheme = "file";
public PhysicalFileProvider(ResourceMetadata metadata = null)
: base(
(metadata ?? ResourceMetadata.Empty)
.Add(ResourceMetadataKeys.CanGet, true)
.Add(ResourceMetadataKeys.CanPut, true)
.Add(ResourceMetadataKeys.CanDelete, true)
.Add(ResourceMetadataKeys.Scheme, Scheme)
)
{ }
public override Task<IResourceInfo> GetAsync(UriString uri, ResourceMetadata metadata = null)
{
ValidateScheme(uri, Scheme);
return Task.FromResult<IResourceInfo>(new PhysicalFileInfo(uri));
}
public override async Task<IResourceInfo> PutAsync(UriString uri, Stream value, ResourceMetadata metadata = null)
{
ValidateScheme(uri, Scheme);
try
{
using (var fileStream = new FileStream(uri.Path, FileMode.CreateNew, FileAccess.Write))
{
await value.CopyToAsync(fileStream);
await fileStream.FlushAsync();
}
return await GetAsync(uri, metadata);
}
catch (Exception inner)
{
throw CreateException(this, uri.Path, metadata, inner);
}
}
public override async Task<IResourceInfo> DeleteAsync(UriString uri, ResourceMetadata metadata = null)
{
ValidateScheme(uri, Scheme);
try
{
File.Delete(uri.Path);
return await GetAsync(uri, metadata);
}
catch (Exception inner)
{
throw CreateException(this, uri.Path, metadata, inner);
}
}
}
public static class ResourceProviderExtensions
{
public static Task<IResourceInfo> GetFileInfoAsync(this IResourceProvider resourceProvider, string path, ResourceMetadata metadata = null)
{
return resourceProvider.GetAsync($"file:{path.Replace('\', '/')}", metadata);
}
}
[PublicAPI]
internal class PhysicalFileInfo : ResourceInfo
{
public PhysicalFileInfo([NotNull] UriString uri) : base(uri) { }
public override bool Exists => File.Exists(Uri.Path);
public override long? Length => new FileInfo(Uri.Path).Length;
public override DateTime? CreatedOn => Exists ? File.GetCreationTimeUtc(Uri.Path) : default;
public override DateTime? ModifiedOn => Exists ? File.GetLastWriteTimeUtc(Uri.Path) : default;
public override async Task CopyToAsync(Stream stream)
{
if (Exists)
{
using (var fileStream = File.OpenRead(Uri.Path))
{
await fileStream.CopyToAsync(stream);
}
}
else
{
throw new InvalidOperationException();
}
}
public override async Task<object> DeserializeAsync(Type targetType)
{
if (Exists)
{
using (var fileStream = File.OpenRead(Uri.Path))
using (var streamReader = new StreamReader(fileStream))
{
return await streamReader.ReadToEndAsync();
}
}
else
{
throw new InvalidOperationException();
}
}
}
This is how I implemented the EmbeddedFileProvider. This one can only do Get.
public class EmbeddedFileProvider : ResourceProvider
{
private readonly Assembly _assembly;
public EmbeddedFileProvider([NotNull] Assembly assembly, ResourceMetadata metadata = null)
: base(
(metadata ?? ResourceMetadata.Empty)
.Add(ResourceMetadataKeys.CanGet, true)
)
{
_assembly = assembly ?? throw new ArgumentNullException(nameof(assembly));
var assemblyName = _assembly.GetName().Name.Replace('.', '/');
BaseUri = new UriString($"file:{assemblyName}");
}
public UriString BaseUri { get; }
#region ResourceProvider
public override Task<IResourceInfo> GetAsync(UriString uri, ResourceMetadata metadata = null)
{
ValidateSchemeNotEmpty(uri);
// Embedded resource names are separated by '.' so replace the windows separator.
var fullUri = new UriString(BaseUri, uri.Path.Value);
var fullName = fullUri.Path.Value.Replace('/', '.');
// Embedded resource names are case sensitive so find the actual name of the resource.
var actualName = _assembly.GetManifestResourceNames().FirstOrDefault(name => SoftString.Comparer.Equals(name, fullName));
var getManifestResourceStream = actualName is null ? default(Func<Stream>) : () => _assembly.GetManifestResourceStream(actualName);
return Task.FromResult<IResourceInfo>(new EmbeddedFileInfo(UndoConvertPath(fullName), getManifestResourceStream));
}
public override Task<IResourceInfo> PutAsync(UriString uri, Stream data, ResourceMetadata metadata = null)
{
throw new NotSupportedException($"{nameof(EmbeddedFileProvider)} does not support value serialization.");
}
public override Task<IResourceInfo> DeleteAsync(UriString uri, ResourceMetadata metadata = null)
{
throw new NotSupportedException($"{nameof(EmbeddedFileProvider)} does not support value deletion.");
}
#endregion
// Convert path back to windows format but the last '.' - this is the file extension.
private static string UndoConvertPath(string path) => Regex.Replace(path, @".(?=.*?.)", Path.DirectorySeparatorChar.ToString());
}
internal class EmbeddedFileInfo : ResourceInfo
{
private readonly Func<Stream> _getManifestResourceStream;
public EmbeddedFileInfo(string uri, Func<Stream> getManifestResourceStream) : base(uri)
{
_getManifestResourceStream = getManifestResourceStream;
}
public override bool Exists => !(_getManifestResourceStream is null);
public override long? Length
{
get
{
using (var stream = _getManifestResourceStream?.Invoke())
{
return stream?.Length;
}
}
}
public override DateTime? CreatedOn { get; }
public override DateTime? ModifiedOn { get; }
public override async Task CopyToAsync(Stream stream)
{
if (Exists)
{
using (var resourceStream = _getManifestResourceStream())
{
await resourceStream.CopyToAsync(stream);
}
}
else
{
throw new InvalidOperationException();
}
}
public override async Task<object> DeserializeAsync(Type targetType)
{
using (var resourceStream = _getManifestResourceStream())
using (var streamReader = new StreamReader(resourceStream))
{
return await streamReader.ReadToEndAsync();
}
}
}
And as the last example the AppSettingProvider form another project that is now also built on top of IResourceProvider:
using static ResourceMetadataKeys;
public class AppSettingProvider : ResourceProvider
{
public AppSettingProvider()
: base(
ResourceMetadata.Empty
.Add(CanGet, true)
.Add(CanPut, true)
)
{ }
public override Task<IResourceInfo> GetAsync(UriString uri, ResourceMetadata metadata = null)
{
var settingName = new SettingName(uri);
var exeConfig = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
var actualKey = FindActualKey(exeConfig, settingName) ?? settingName;
var element = exeConfig.AppSettings.Settings[actualKey];
return Task.FromResult<IResourceInfo>(new AppSettingInfo(uri, element?.Value));
}
public override async Task<IResourceInfo> PutAsync(UriString uri, Stream stream, ResourceMetadata metadata = null)
{
using (var valueReader = new StreamReader(stream))
{
var value = await valueReader.ReadToEndAsync();
var settingName = new SettingName(uri);
var exeConfig = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
var actualKey = FindActualKey(exeConfig, settingName) ?? settingName;
var element = exeConfig.AppSettings.Settings[actualKey];
if (element is null)
{
exeConfig.AppSettings.Settings.Add(settingName, (string)value);
}
else
{
exeConfig.AppSettings.Settings[actualKey].Value = (string)value;
}
exeConfig.Save(ConfigurationSaveMode.Minimal);
return await GetAsync(uri);
}
}
public override Task<IResourceInfo> DeleteAsync(UriString uri, ResourceMetadata metadata = null)
{
throw new NotImplementedException();
}
[CanBeNull]
private static string FindActualKey(System.Configuration.Configuration exeConfig, string key)
{
return
exeConfig
.AppSettings
.Settings
.AllKeys
.FirstOrDefault(k => SoftString.Comparer.Equals(k, key));
}
}
internal class AppSettingInfo : ResourceInfo
{
[CanBeNull]
private readonly string _value;
internal AppSettingInfo([NotNull] UriString uri, [CanBeNull] string value) : base(uri)
{
_value = value;
}
public override bool Exists => !(_value is null);
public override long? Length => _value?.Length;
public override DateTime? CreatedOn { get; }
public override DateTime? ModifiedOn { get; }
public override async Task CopyToAsync(Stream stream)
{
if (Exists)
{
// ReSharper disable once AssignNullToNotNullAttribute - this isn't null here
using (var valueStream = _value.ToStreamReader())
{
await valueStream.BaseStream.CopyToAsync(stream);
}
}
}
public override Task<object> DeserializeAsync(Type targetType)
{
return Task.FromResult<object>(_value);
}
}
So, how do you like it and how would you improve it? There are not many properties and some things are hidden inside ResourceMetadata but with a single API for everything you must make some tradeoffs. I find works surprisingly well.
(In future, when I will need for such nichts as Ftp, I will add a new API to the ResourceProvider that I will probably call BeginScope/Session to be able to work with sessions.)
I call it IOnymous and the complete code is as always on GitHub in the experimental branch.
c# api io dependency-injection framework
c# api io dependency-injection framework
edited 3 hours ago
asked 3 hours ago
t3chb0t
33.8k746111
33.8k746111
add a comment |
add a comment |
active
oldest
votes
Your Answer
StackExchange.ifUsing("editor", function () {
return StackExchange.using("mathjaxEditing", function () {
StackExchange.MarkdownEditor.creationCallbacks.add(function (editor, postfix) {
StackExchange.mathjaxEditing.prepareWmdForMathJax(editor, postfix, [["\$", "\$"]]);
});
});
}, "mathjax-editing");
StackExchange.ifUsing("editor", function () {
StackExchange.using("externalEditor", function () {
StackExchange.using("snippets", function () {
StackExchange.snippets.init();
});
});
}, "code-snippets");
StackExchange.ready(function() {
var channelOptions = {
tags: "".split(" "),
id: "196"
};
initTagRenderer("".split(" "), "".split(" "), channelOptions);
StackExchange.using("externalEditor", function() {
// Have to fire editor after snippets, if snippets enabled
if (StackExchange.settings.snippets.snippetsEnabled) {
StackExchange.using("snippets", function() {
createEditor();
});
}
else {
createEditor();
}
});
function createEditor() {
StackExchange.prepareEditor({
heartbeatType: 'answer',
autoActivateHeartbeat: false,
convertImagesToLinks: false,
noModals: true,
showLowRepImageUploadWarning: true,
reputationToPostImages: null,
bindNavPrevention: true,
postfix: "",
imageUploader: {
brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
allowUrls: true
},
onDemand: true,
discardSelector: ".discard-answer"
,immediatelyShowMarkdownHelp:true
});
}
});
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f209939%2fmultiple-file-access-abstractions-follow-up-redesign%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
active
oldest
votes
active
oldest
votes
active
oldest
votes
active
oldest
votes
Thanks for contributing an answer to Code Review Stack Exchange!
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
Use MathJax to format equations. MathJax reference.
To learn more, see our tips on writing great answers.
Some of your past answers have not been well-received, and you're in danger of being blocked from answering.
Please pay close attention to the following guidance:
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
To learn more, see our tips on writing great answers.
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f209939%2fmultiple-file-access-abstractions-follow-up-redesign%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown